# Get all bundle rules Source: https://developers.appstle.com/bundles-admin-api/bundle-rules/get-all-bundle-rules /bundles/admin-api-swagger.json get /api/external/bundle-rules # Get build-a-box bundle rules Source: https://developers.appstle.com/bundles-admin-api/bundle-rules/get-build-a-box-bundle-rules /bundles/admin-api-swagger.json get /api/external/bundle-rules/bab # Get discount rules Source: https://developers.appstle.com/bundles-admin-api/bundle-rules/get-discount-rules /bundles/admin-api-swagger.json get /api/external/bundle-rules/discount # Post bundlesbbapiauto add cart rulesevaluate Source: https://developers.appstle.com/bundles-storefront-api/auto-add-cart-rule-storefront-resource/post-bundlesbbapiauto-add-cart-rulesevaluate /bundles/storefront-api-swagger.json post /bundles/bb/api/auto-add-cart-rules/evaluate # Generate a build-a-box bundle discount Source: https://developers.appstle.com/bundles-storefront-api/build-a-box/generate-a-build-a-box-bundle-discount /bundles/storefront-api-swagger.json put /bundles/bb/api/build-a-box/discount/{token} # Generate a build-a-box shipping discount Source: https://developers.appstle.com/bundles-storefront-api/build-a-box/generate-a-build-a-box-shipping-discount /bundles/storefront-api-swagger.json put /bundles/bb/api/build-a-box/shipping-discount/{token} # Get build-a-box configuration by handle Source: https://developers.appstle.com/bundles-storefront-api/build-a-box/get-build-a-box-configuration-by-handle /bundles/storefront-api-swagger.json get /bundles/bb/api/build-a-box/get-bundle/{handle} # Get build-a-box configuration by token Source: https://developers.appstle.com/bundles-storefront-api/build-a-box/get-build-a-box-configuration-by-token /bundles/storefront-api-swagger.json get /bundles/bb/api/build-a-box/by-token/{token} # Get single-product build-a-box configuration Source: https://developers.appstle.com/bundles-storefront-api/build-a-box/get-single-product-build-a-box-configuration /bundles/storefront-api-swagger.json get /bundles/bb/api/build-a-box/single-product # Proxy a Shopify Storefront GraphQL request for bundle widgets Source: https://developers.appstle.com/bundles-storefront-api/product-catalog/proxy-a-shopify-storefront-graphql-request-for-bundle-widgets /bundles/storefront-api-swagger.json post /bundles/bb/api/storefront-graphql # Proxy a Shopify Storefront GraphQL request from the customer portal Source: https://developers.appstle.com/bundles-storefront-api/product-catalog/proxy-a-shopify-storefront-graphql-request-from-the-customer-portal /bundles/storefront-api-swagger.json post /bundles/cp/api/storefront-graphql # Record a storefront visitor ping Source: https://developers.appstle.com/bundles-storefront-api/storefront/record-a-storefront-visitor-ping /bundles/storefront-api-swagger.json post /bundles/cp/api/ping-visitor # Render the build-a-box storefront page Source: https://developers.appstle.com/bundles-storefront-api/storefront/render-the-build-a-box-storefront-page /bundles/storefront-api-swagger.json get /bundles/bb/{token} # Authenticate with the Appstle Bundles API Source: https://developers.appstle.com/bundles/authentication Create an Appstle API key and pass it in the X-API-Key header for Bundles Admin API requests. Every Bundles Admin API request must carry a valid API key. Keys are created in the Appstle dashboard and scoped to a single Shopify store. ## Creating an API key Log in to your Appstle admin panel and navigate to **Settings → API Key Management**. Click **Create New Key** and enter a descriptive name such as `Bundles integration` or `ERP bundle sync`. The full key value is shown only once. Store it in your secrets manager before leaving the page. API keys grant access to store data. Never expose them in client-side JavaScript, browser extensions, mobile apps, or public repositories. ## Using the API key Include your key in the `X-API-Key` header on every Admin API request. ```bash curl theme={null} curl -X GET "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=your-store.myshopify.com" -H "X-API-Key: apst_your-api-key-here" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://bundles-admin.appstle.com/api/external/bundle-rules?shop=your-store.myshopify.com', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); const bundleRules = await response.json(); ``` ```python Python theme={null} import os import httpx response = httpx.get( 'https://bundles-admin.appstle.com/api/external/bundle-rules', params={'shop': 'your-store.myshopify.com'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) print(response.json()) ``` Create one API key per integration. If a tool is compromised or decommissioned, you can revoke that key without disrupting other integrations. ## Storefront endpoints Storefront endpoints power build-a-box and customer-facing bundle flows. They are documented separately from the Admin API because they are meant to be called in storefront contexts rather than backend integrations. # Dynamic pricing bundle with a custom storefront Source: https://developers.appstle.com/bundles/dynamic-pricing-bundle Offer a build-your-own bundle that applies a percentage or fixed-amount discount to the running total, using your own storefront UI and Appstle's automatic discount. A **dynamic pricing bundle** lets customers build their own selection from an eligible set, and applies a discount to the running total — either a **percentage** (e.g. *"15% off when you bundle any 3"*) or a **fixed amount** (e.g. *"\$10 off your build-your-own box"*). Unlike a fixed pricing bundle, the total scales with what the customer picks; the discount is layered on top. This guide covers running a dynamic pricing bundle from your own storefront UI. You use the *same* metafield, cart-line attributes, and Shopify Function that Appstle's built-in widget uses — only the UI is yours. Read the [headless overview](/bundles/headless-overview) first for the automatic-discount model. | | | | ----------------------- | --------------------------------------------- | | **`bundleType`** | `CLASSIC_BUILD_A_BOX` | | **Read from** | `appstle_bundles.bundle_rules` shop metafield | | **Discount applied by** | Appstle automatic discount (Shopify Function) | ## How pricing works The rule defines a `discountType` and `discountValue`, plus optional selection gates: | `discountType` | Discount on the eligible subtotal | | -------------- | ---------------------------------------------------- | | `PERCENTAGE` | `subtotal × (discountValue / 100)` off | | `FIXED_AMOUNT` | `discountValue` off | | `NO_DISCOUNT` | No discount (the bundle exists purely for grouping). | The discount applies only when the selection satisfies the configured gates: * **Item count** between `minProductCount` and `maxProductCount`. * **Selection value** between `minOrderAmount` and `maxOrderAmount`. So the customer-facing math is: ``` subtotal = Σ (line price × quantity) // eligible bundle lines discount = PERCENTAGE → subtotal × value/100 FIXED_AMOUNT → value customer pays = subtotal − discount // when gates are satisfied ``` Appstle's Function evaluates this and applies the discount automatically once the lines carry the bundle attributes. ## Step 1 — Read the rule from the metafield Read the active rules from the **`appstle_bundles.bundle_rules`** shop metafield — the Shopify-native source, with no call to Appstle's servers (see [Reading bundle configuration](/bundles/headless-overview#reading-bundle-configuration)). Filter to dynamic pricing bundles by `bundleType`. ```liquid theme={null} {% assign bundle_rules = shop.metafields.appstle_bundles.bundle_rules.value %} ``` ```javascript theme={null} const rules = window.MY_BUNDLE_RULES || []; const dynamicBundles = rules.filter( r => r.bundleType === 'CLASSIC_BUILD_A_BOX' && r.status === 'ACTIVE' ); ``` Fields you need: | Field | Use | | ------------------------------------- | ----------------------------------------------- | | `uniqueRef` | Attach to every cart line as `_appstle-bb-id`. | | `name` | Display label; attach as `__appstle-bb-name`. | | `discountType` | `PERCENTAGE`, `FIXED_AMOUNT`, or `NO_DISCOUNT`. | | `discountValue` | The percentage or fixed amount. | | `minProductCount` / `maxProductCount` | Item-count gate. | | `minOrderAmount` / `maxOrderAmount` | Selection-value gate. | | `products` / `variants` | Eligible products and variants (JSON). | ## Step 2 — Render your selector Build your own selector and gate the "add to cart" action on the rule: * Allow only eligible variants. * Track the running item count and subtotal; enable checkout only when both gates are satisfied. * Show the live discounted total using the [preview formula](#previewing-the-discount). ## Step 3 — Add the bundle to the cart Add the selected variants with the [bundle attributes](/bundles/headless-overview#required-cart-line-attributes). For a dynamic pricing bundle, `_appstle_bundles_type` is `CLASSIC_BUILD_A_BOX`. ```js Shopify theme (Ajax Cart API) theme={null} const line = (variantId, quantity, sellingPlanId = null) => ({ id: variantId, quantity, ...(sellingPlanId ? { selling_plan: sellingPlanId } : {}), properties: { '_appstle-bb-id': bundle.uniqueRef, '_appstle_bundles_type': 'CLASSIC_BUILD_A_BOX', '__appstle-bb-name': bundle.name, }, }); await fetch(`${Shopify.routes.root}cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: selection.map(s => line(s.variantId, s.quantity)) }), }); ``` ```graphql Headless (Storefront API) theme={null} mutation AddDynamicBundle($cartId: ID!) { cartLinesAdd( cartId: $cartId lines: [ { merchandiseId: "gid://shopify/ProductVariant/123" quantity: 2 attributes: [ { key: "_appstle-bb-id", value: "byo-box-ref" } { key: "_appstle_bundles_type", value: "CLASSIC_BUILD_A_BOX" } { key: "__appstle-bb-name", value: "Build Your Own Box" } ] } ] ) { cart { id } userErrors { field message } } } ``` Dynamic pricing bundles support subscriptions. If the selection uses a subscription purchase option, pass the chosen `selling_plan` on the line (Ajax) or `sellingPlanId` on the merchandise line (Storefront API), exactly as you would for any subscription line item. ## Step 4 — Discount applies automatically When the lines carrying the attributes satisfy the gates, Appstle's automatic discount applies the percentage or fixed amount in cart and checkout. If the selection later drops below the gate (e.g. an item is removed), the Function stops applying the discount — your UI does not need to manage this. ## Previewing the discount Mirror the Function's math for a live preview (cart prices are in cents): ```js theme={null} function previewDynamicDiscount(bundle, selection) { const subtotalCents = selection.reduce((s, i) => s + i.priceCents * i.quantity, 0); const count = selection.reduce((n, i) => n + i.quantity, 0); // Gates — the discount only applies when these pass. const countOk = (bundle.minProductCount == null || count >= bundle.minProductCount) && (bundle.maxProductCount == null || count <= bundle.maxProductCount); const amountOk = (bundle.minOrderAmount == null || subtotalCents >= bundle.minOrderAmount * 100) && (bundle.maxOrderAmount == null || subtotalCents <= bundle.maxOrderAmount * 100); if (!countOk || !amountOk) return { subtotalCents, discountCents: 0, totalCents: subtotalCents }; let discountCents = 0; if (bundle.discountType === 'PERCENTAGE') { discountCents = Math.round(subtotalCents * (bundle.discountValue / 100)); } else if (bundle.discountType === 'FIXED_AMOUNT') { discountCents = Math.round(bundle.discountValue * 100); } discountCents = Math.min(discountCents, subtotalCents); return { subtotalCents, discountCents, totalCents: subtotalCents - discountCents }; } ``` This preview is display-only. The Shopify Function is authoritative at checkout; sourcing the preview from the same `discountType` / `discountValue` / gate fields keeps them in sync. ## Checklist ## Next steps Tiered "buy more, save more" discounts. Sell a curated set for one fixed price. # Fixed pricing bundle with a custom storefront Source: https://developers.appstle.com/bundles/fixed-pricing-bundle Sell a curated set of products for one fixed total price using your own storefront UI. Read the rule, render a selector, add to cart with Appstle's bundle attributes, and let the automatic discount price the set. A **fixed pricing bundle** sells a defined set of products for one fixed total price, regardless of the sum of the individual item prices. For example, *"Any 3 candles for \$40"* — if the candles total \$54, the customer saves \$14 automatically. This guide shows how to run a fixed pricing bundle entirely from your own storefront UI. You use the *same* metafield, cart-line attributes, and Shopify Function that Appstle's built-in widget uses — only the UI is yours. If you have not yet read the [headless overview](/bundles/headless-overview), start there — it explains the automatic-discount model these steps rely on. | | | | ----------------------- | --------------------------------------------- | | **`bundleType`** | `SINGLE_PRODUCT_BUILD_A_BOX` | | **`bundleSubType`** | `FIXED_BUNDLE` | | **Read from** | `appstle_bundles.bundle_rules` shop metafield | | **Discount applied by** | Appstle automatic discount (Shopify Function) | ## How pricing works The merchant sets a single `price` on the rule — the total the customer pays for the bundle. Appstle's discount Function reduces the bundled lines so their combined total equals `price`: ``` customer pays = price customer saves = (sum of selected line prices) − price ``` The discount is applied automatically at checkout once the lines carry the bundle attributes. You do not compute or submit the discount. ## Step 1 — Read the rule from the metafield Read the active rules from the **`appstle_bundles.bundle_rules`** shop metafield — the Shopify-native source, with no call to Appstle's servers (see [Reading bundle configuration](/bundles/headless-overview#reading-bundle-configuration)). Filter to fixed pricing bundles by `bundleType` + `bundleSubType`. ```liquid theme={null} {% assign bundle_rules = shop.metafields.appstle_bundles.bundle_rules.value %} ``` ```javascript theme={null} const rules = window.MY_BUNDLE_RULES || []; const fixedBundles = rules.filter( r => r.bundleType === 'SINGLE_PRODUCT_BUILD_A_BOX' && r.bundleSubType === 'FIXED_BUNDLE' && r.status === 'ACTIVE' ); ``` The fields you need from each rule: | Field | Use | | ------------------------------------- | ---------------------------------------------- | | `uniqueRef` | Attach to every cart line as `_appstle-bb-id`. | | `name` | Display label; attach as `__appstle-bb-name`. | | `price` | The fixed total the customer pays. | | `minProductCount` / `maxProductCount` | How many items the customer must pick. | | `products` / `variants` | Eligible products and variants (JSON). | Take the eligible product and variant IDs from the rule's `products` / `variants` fields and hydrate titles, images, and prices with **Shopify's own** Storefront API or Liquid product objects. If you would rather receive a pre-parsed product list from Appstle, the App Proxy endpoint `GET /bundles/bb/api/build-a-box/get-bundle/{handle}` returns one — use it as a last resort and cache the response. ## Step 2 — Render your selector Build whatever UI matches your theme. Drive its rules from the configuration: * Show the eligible products. * Enforce the item count between `minProductCount` and `maxProductCount`. * Display the headline price (`price`) prominently, and optionally the savings (see [below](#previewing-savings)). ## Step 3 — Add the bundle to the cart When the selection is valid, add the chosen variants to the cart with the [required attributes](/bundles/headless-overview#required-cart-line-attributes). For a fixed pricing bundle, each component line carries `_appstle-bb-id`, `_appstle_bundles_type`, and `__appstle-bb-name`. ```js Shopify theme (Ajax Cart API) theme={null} const lineFor = (variantId, quantity) => ({ id: variantId, quantity, properties: { '_appstle-bb-id': bundle.uniqueRef, '_appstle_bundles_type': 'SINGLE_PRODUCT_BUILD_A_BOX', '__appstle-bb-name': bundle.name, }, }); await fetch(`${Shopify.routes.root}cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: selectedItems.map(i => lineFor(i.variantId, i.quantity)), }), }); ``` ```graphql Headless (Storefront API) theme={null} mutation AddFixedBundle($cartId: ID!) { cartLinesAdd( cartId: $cartId lines: [ { merchandiseId: "gid://shopify/ProductVariant/111" quantity: 1 attributes: [ { key: "_appstle-bb-id", value: "fixed-candle-set-ref" } { key: "_appstle_bundles_type", value: "SINGLE_PRODUCT_BUILD_A_BOX" } { key: "__appstle-bb-name", value: "Any 3 Candles" } ] } { merchandiseId: "gid://shopify/ProductVariant/222" quantity: 1 attributes: [ { key: "_appstle-bb-id", value: "fixed-candle-set-ref" } { key: "_appstle_bundles_type", value: "SINGLE_PRODUCT_BUILD_A_BOX" } { key: "__appstle-bb-name", value: "Any 3 Candles" } ] } ] ) { cart { id } userErrors { field message } } } ``` Every line that belongs to the bundle must carry the **same** `_appstle-bb-id`. A line without the attributes is treated as a normal product and is not priced into the fixed total. ## Step 4 — Discount applies automatically Once the lines are in the cart, Appstle's automatic discount reduces the bundled lines so the set totals `price`. It shows in the cart, persists through checkout, and is reflected on the order. There is nothing else to call. ## Previewing savings To show the customer what they save before checkout, compute it from the rule (remember cart prices are in cents): ```js theme={null} // linePrices: array of unit prices (in cents) × quantities for the selected items const subtotal = linePrices.reduce((sum, cents) => sum + cents, 0); const fixedTotalCents = Math.round(bundle.price * 100); const savingsCents = Math.max(0, subtotal - fixedTotalCents); // e.g. "You save $14.00" const savingsLabel = `You save ${(savingsCents / 100).toFixed(2)}`; ``` This is a display-only preview. The Shopify Function applies the authoritative discount at checkout from the same `price`, so the two always agree. ## Checklist Only active rules have a live automatic discount. Validate before adding to cart. `_appstle-bb-id` (= `uniqueRef`) and `_appstle_bundles_type` (= `SINGLE_PRODUCT_BUILD_A_BOX`) on each line. Confirm the bundled lines total the configured `price`. ## Next steps Build-your-own with a percentage or amount discount. Tiered "buy more, save more" discounts. # Build bundles with your own storefront UI Source: https://developers.appstle.com/bundles/headless-overview Run Appstle's fixed pricing, dynamic pricing, and volume discount bundles with a fully custom storefront. Read the configuration from Shopify metafields, render your own UI with Shopify's own APIs, and let Appstle's Shopify Function apply the discount — with little or no traffic to Appstle's servers. Many merchants love how Appstle Bundles calculates and applies discounts but want to design the customer-facing experience themselves so it matches their theme. Appstle fully supports this **headless** (or *bring-your-own-UI*) model for the three most common bundle types: * **Fixed pricing bundle** — a set of products sold for one fixed total price. * **Dynamic pricing bundle** — a build-your-own selection with a percentage or fixed-amount discount applied to the running total. * **Volume discount bundle** — tiered "buy more, save more" discounts based on quantity or spend. **The mental model in one sentence:** you use the *exact same* Shopify metafields, cart-line attributes, and discount Functions that Appstle's own bundle widgets use — the only thing you replace is the UI. Nothing about how the bundle is configured, priced, or discounted changes; you are simply rendering the experience yourself instead of letting Appstle's widget render it. This page explains the architecture that makes headless integrations possible. The per-bundle guides then walk through each type end to end. Sell a curated set for one fixed price. Build-your-own with a percentage or amount discount. Tiered "buy more, save more" discounts. ## Design principle: lean on Shopify, not on Appstle **Read configuration from Shopify metafields, fetch product data from Shopify's own APIs, compute previews on the client, and let the discount apply through a Shopify Function.** Treat calls to Appstle's servers as a last resort, and cache them when you do make them. This is exactly how Appstle's own storefront widgets are built — they read everything from a Shopify metafield and almost never call back to Appstle at render time. Following the same pattern keeps your storefront fast and resilient, and avoids putting storefront-scale traffic on Appstle's API. The key idea: **you never calculate or apply the final discount yourself**. The merchant configures the bundle once in the Appstle admin, and Appstle provisions a **Shopify automatic discount backed by a Shopify Function** in the store, and publishes the rules to a **shop metafield**. Your storefront reads the rules from that metafield, renders a selector, and adds the customer's chosen line items to the cart **with a small set of line-item attributes**. The Appstle discount Function detects those attributes and applies the savings automatically — in the cart, in checkout, and on the final order. ```mermaid theme={null} flowchart LR A[Merchant configures bundle
in Appstle admin] --> B[Appstle publishes rules to a
Shopify metafield + provisions
an automatic discount Function] B --> C[Your storefront reads the rules
from the Shopify metafield] C --> D[You render your own selector
using Shopify product data] D --> E[Add line items to cart
with _appstle-* attributes] E --> F[Appstle Function detects the
attributes and applies the discount] ``` This design has three important consequences: Discounts are **automatic**, not code-based. You do not request, generate, or apply a discount code anywhere in the flow. The Shopify Function decides the final discount at checkout. Any number you show in your UI is a **preview** — compute it from the same rule fields. If the required `_appstle-*` line attributes are missing, the Function cannot recognize the line and **no discount is applied**. The legacy `PUT /bundles/bb/api/build-a-box/discount/{token}` endpoint is **deprecated** and no longer returns a usable discount — discounting moved entirely to automatic Shopify Functions. Do not build a headless flow around it. Apply the line attributes described below instead. ## Prerequisites The discount Functions and the rules metafield only exist while the app is installed. Uninstalling removes the automatic discounts. Configure the bundle (products, pricing, tiers) in the Appstle dashboard and set its status to **Active**. This provisions the Shopify automatic discount and publishes the rule to the shop metafield. Bundles are authored in the admin; there is no API to create them. On a Shopify theme, enable the Appstle Bundles app embed/block so the rules metafield is available to your code. You can hide Appstle's default widget and render your own UI from the same data. ## The integration model Every headless bundle, regardless of type, follows the same shape — and most of it runs against Shopify, not Appstle: Appstle publishes active rules to the **`appstle_bundles.bundle_rules`** shop metafield. Read it the Shopify-native way — see [Reading bundle configuration](#reading-bundle-configuration). No Appstle server call. Build UI from the rule fields: list eligible products, enforce the selection limits, and show a running total. Use **Shopify's own** product/variant data (Liquid objects or the Storefront API) for titles, images, and prices. When the selection is valid, add the line items via Shopify's **Ajax Cart API** (themes) or the **Storefront API `cartLinesAdd`** mutation (fully headless), attaching the [bundle line attributes](#required-cart-line-attributes). Appstle's discount Function reads the attributes and applies the savings in cart and checkout. You do nothing else. Optionally render a [discount preview](#previewing-the-discount) computed on the client from the same rule fields. ## Reading bundle configuration Pick the source closest to Shopify. The first option should cover almost every storefront. Appstle publishes all active bundle rules to the shop metafield **`appstle_bundles.bundle_rules`**. This is the same data that drives Appstle's own widgets (exposed there as `window._ABConfig.bundle_rules`), and it contains every rule type — fixed pricing, dynamic pricing, and volume discount. On a Shopify theme, read it directly in Liquid — **no network call to anyone**: ```liquid theme={null} {% assign bundle_rules = shop.metafields.appstle_bundles.bundle_rules.value %} ``` Then filter to the bundle you want by `bundleType` and `uniqueRef`: ```js theme={null} const rules = window.MY_BUNDLE_RULES || []; const volumeBundles = rules.filter(r => r.bundleType === 'VOLUME_DISCOUNT' && r.status === 'ACTIVE'); ``` Several rule fields are themselves **JSON-encoded strings** inside the metafield (for example `tieredDiscount`, `products`, `variants`). Parse them with `JSON.parse(...)` before use — exactly as Appstle's own widget does. On a non-theme stack (Hydrogen, a custom frontend), fetch the **same shop metafield** through Shopify's Storefront API rather than calling Appstle: ```graphql theme={null} query BundleRules { shop { metafield(namespace: "appstle_bundles", key: "bundle_rules") { value } } } ``` `value` is a JSON string — parse it into the rules array. Hydrate product titles, images, and prices with Shopify's Storefront API as usual. If the metafield is not exposed to the Storefront API for your store, read it server-side via the Shopify Admin API and cache it. Appstle exposes build-a-box configuration through your store's **Shopify App Proxy**. Use these only when you need server-parsed product hydration that you cannot get from Shopify directly, and **cache the responses** — they are not meant for per-request, storefront-scale traffic. | Endpoint (via App Proxy) | Returns | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /bundles/bb/api/build-a-box/by-token/{token}` | A single bundle rule, where `{token}` is the rule's `uniqueRef`. | | `GET /bundles/bb/api/build-a-box/get-bundle/{handle}` | The rule plus a **parsed `products` array** (`productId`, `variantId`, `name`, `productHandle`, `imageSrc`, `price`, `minQuantity`, `maxQuantity`, `mandatory`, …) ready to render. | | `GET /bundles/bb/api/build-a-box/single-product` | All active fixed-pricing build-a-box rules for the shop. | For **server-side** use cases — ERP sync, reporting, internal tooling — read rules with the `X-API-Key` Admin API. This requires a secret key and **must never run in a browser or storefront context**. See [Authentication](/bundles/authentication). ```bash theme={null} curl -X GET \ "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=${SHOP}&status.equals=ACTIVE" \ -H "X-API-Key: ${APPSTLE_API_KEY}" ``` ## Bundle type reference Each Appstle bundle type maps to a `bundleType` value in the rules data. These three are supported for headless storefronts: | Merchant label | `bundleType` | `bundleSubType` | | ---------------------- | ---------------------------- | --------------- | | Fixed pricing bundle | `SINGLE_PRODUCT_BUILD_A_BOX` | `FIXED_BUNDLE` | | Dynamic pricing bundle | `CLASSIC_BUILD_A_BOX` | — | | Volume discount bundle | `VOLUME_DISCOUNT` | — | Other values exist in the `bundleType` enum (for example `DISCOUNTED_PRICING`, `BUY_X_GET_Y`, `SECTIONED_BUNDLE`, `COMBO_BUNDLE`). This documentation covers the three types above; the same metafield-plus-attributes model extends to the others. ### Discount fields on a rule The relevant pricing fields on each rule object: | Field | Type | Used by | Meaning | | ------------------------------------- | ------------- | --------------- | ---------------------------------------------------------------------------------------------------------- | | `uniqueRef` | string | all | Stable reference for the bundle. **You attach this to every cart line** as `_appstle-bb-id`. | | `discountType` | enum | dynamic, volume | One of `PERCENTAGE`, `FIXED_AMOUNT`, `FIXED_BUNDLE_AMOUNT`, `TIERED_DISCOUNT`, `NO_DISCOUNT`, `FREE_GIFT`. | | `discountValue` | number | dynamic | The percentage (e.g. `15` = 15%) or fixed amount, depending on `discountType`. | | `price` | number | fixed | The fixed total price the customer pays for the bundle. | | `tieredDiscount` | string (JSON) | volume | JSON array of tier objects (see the [Volume discount guide](/bundles/volume-discount-bundle)). | | `minProductCount` / `maxProductCount` | integer | dynamic, fixed | Min / max number of items the customer must select. | | `minOrderAmount` / `maxOrderAmount` | number | dynamic | Min / max selection value required for the discount to apply. | | `products` / `variants` | string (JSON) | all | Eligible products and variants configured for the bundle. | | `status` | string | all | The bundle is only live when `ACTIVE`. | ## Required cart line attributes This is the contract between your storefront and Appstle's discount Function. When you add a bundle line item to the cart, attach these attributes. On a Shopify theme they are passed as line-item **`properties`**; through the Storefront API they are line **`attributes`**. Both surface to the Function as cart-line attributes. | Attribute key | Required | Value | Purpose | | -------------------------- | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------ | | `_appstle-bb-id` | **Yes** | The rule's `uniqueRef` | Links the line to a specific bundle so the Function knows which rule to apply. | | `_appstle_bundles_type` | **Yes** | The `bundleType` value (e.g. `VOLUME_DISCOUNT`) | Tells the Function which discount logic to run. | | `__appstle-bb-name` | Recommended | The bundle's `name` | Human-readable label shown in cart/order line items. | | `_appstle_bundle_combo_id` | Volume only (tier restriction) | The selected tier's identifier | Pins the line to a specific tier when *tier restriction* is enabled. | | `_appstle-bb-child` | Fixed only (child items) | `"true"` | Marks component lines added alongside the parent in certain inventory modes. | Attribute **keys are matched exactly**, including the leading underscore and the mix of hyphens and underscores (`_appstle-bb-id` vs. `_appstle_bundles_type`). Copy them verbatim. Keys beginning with `_` are hidden from the customer in standard Shopify cart/checkout UIs. ### Adding to cart on a Shopify theme (Ajax Cart API) ```js theme={null} await fetch(`${Shopify.routes.root}cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [ { id: variantId, // numeric Shopify variant ID quantity: 2, properties: { '_appstle-bb-id': bundle.uniqueRef, '_appstle_bundles_type': bundle.bundleType, '__appstle-bb-name': bundle.name, }, }, ], }), }); ``` ### Adding to cart on a fully headless stack (Storefront API) ```graphql theme={null} mutation AddBundleLines($cartId: ID!) { cartLinesAdd( cartId: $cartId lines: [ { merchandiseId: "gid://shopify/ProductVariant/123456789" quantity: 2 attributes: [ { key: "_appstle-bb-id", value: "your-bundle-uniqueRef" } { key: "_appstle_bundles_type", value: "VOLUME_DISCOUNT" } { key: "__appstle-bb-name", value: "Summer Skincare Set" } ] } ] ) { cart { id } userErrors { field message } } } ``` ## Previewing the discount Because the Shopify Function applies the authoritative discount at checkout, any amount you display beforehand is a **preview** — and you compute it **on the client**, from the same rule fields, with no server call. (Appstle's own widget does the same, and even exposes a `window._ABConfig.getProductDiscountedPricing(...)` helper on theme storefronts.) The formula differs per type — see each guide: * [Fixed pricing](/bundles/fixed-pricing-bundle#previewing-savings) — savings = sum of line prices − `price`. * [Dynamic pricing](/bundles/dynamic-pricing-bundle#previewing-the-discount) — percentage or fixed amount off the eligible subtotal. * [Volume discount](/bundles/volume-discount-bundle#previewing-the-discount) — the best-matching tier's discount. All monetary values in the Shopify cart are expressed in the **minor unit** (cents). A `total_price` of `5000` means `$50.00`. Account for this when computing previews. ## Next steps End-to-end headless walkthrough for fixed-price sets. End-to-end headless walkthrough for build-your-own discounts. End-to-end headless walkthrough for tiered savings. For backend-only reads — never used from a storefront. # Appstle Bundles integration guide Source: https://developers.appstle.com/bundles/integration-guide Build a production integration with Appstle Bundles. Covers base URLs, authentication, bundle rules, build-a-box flows, and discount generation. This guide covers the main integration patterns for Appstle Bundles: reading merchant bundle configuration from a backend service and powering build-a-box flows on the storefront. ## Base URL All Bundles endpoints currently use this host: ```text theme={null} https://bundles-admin.appstle.com ``` Admin endpoints are prefixed with `/api/external/`. Storefront endpoints are documented in the generated Storefront API reference. ## Authentication Direct backend integrations pass the merchant's API key in the `X-API-Key` header. ```bash theme={null} curl -H "X-API-Key: apst_your-api-key-here" "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=your-store.myshopify.com" ``` Keep this key server-side only. Storefront endpoints support customer-facing build-a-box flows. They use tokenized storefront URLs and request parameters documented in the Storefront API reference. ## Admin API endpoints ### Get bundle rules Retrieve configured bundle rules for a store. ```bash theme={null} curl -X GET "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=your-store.myshopify.com" -H "X-API-Key: YOUR_API_KEY" ``` ### Get discount rules Retrieve discount-oriented bundle rules. ```bash theme={null} curl -X GET "https://bundles-admin.appstle.com/api/external/bundle-rules/discount?shop=your-store.myshopify.com" -H "X-API-Key: YOUR_API_KEY" ``` ### Get build-a-box rules Retrieve build-a-box bundle rules. ```bash theme={null} curl -X GET "https://bundles-admin.appstle.com/api/external/bundle-rules/bab?shop=your-store.myshopify.com" -H "X-API-Key: YOUR_API_KEY" ``` ## Storefront API endpoints Use the Storefront API reference for exact schemas. The main categories are: Fetch build-a-box configuration by token, handle, or single-product context so a custom storefront can render the correct bundle experience. Generate bundle discounts and shipping discounts for eligible build-a-box selections. Ping visitor activity for Appstle storefront bundle widgets and customer-facing flows. ## Production checklist * Keep Admin API keys server-side. * Cache bundle-rule reads where possible; merchant configuration changes less frequently than storefront traffic. * Treat Storefront API token parameters as flow-specific and avoid hard-coding them across shops. * Implement retries with exponential backoff for `429` and transient `5xx` responses. * Log request IDs and response status codes for merchant-support debugging. # Appstle Bundles: Build-a-Box and Bundle Rules Source: https://developers.appstle.com/bundles/introduction Appstle Bundles provides Admin and Storefront REST APIs to read bundle rules, power build-a-box experiences, and generate bundle discounts on Shopify. Appstle Bundles helps Shopify merchants create bundle offers, build-a-box flows, and discount experiences. Read configured bundle rules from your server, render build-a-box experiences from the storefront, and — when you want full control of the look and feel — run Appstle's fixed pricing, dynamic pricing, and volume discount bundles with your own custom storefront UI. Prefer your own design over Appstle's built-in widgets? Read the bundle rule over the API, render your own selector, and let Appstle's automatic discount apply the savings at checkout. Start with the headless overview. ## Available APIs Appstle Bundles exposes two REST API surfaces. Pick the one that matches where your code runs: * **Admin API** — server-side, authenticated with `X-API-Key`. For reading configured bundle and discount rules from a backend service. Browse the full reference under **Admin API** in the sidebar. * **Storefront API** — customer-facing, accessed through Shopify App Proxy. For rendering build-a-box experiences and generating bundle discounts. Browse the full reference under **Storefront API** in the sidebar. ## Which API should you use? * You need to **read configured bundle rules** from a backend service * You need to **read bundle discount rules** * You are **syncing build-a-box rules** into another system * You are building **reporting, auditing, or merchant-support tooling** * Your code runs **anywhere outside a customer's browser** on the storefront Admin API requests require an `X-API-Key` header. Keys are created in the Appstle dashboard under **Settings → API Key Management**. Never expose keys in client-side code. * You are **rendering build-a-box pages** on your storefront * You are **looking up bundle configuration** by token or product handle * You are **generating bundle and shipping discounts** for a selection * You are sending **storefront widget activity** calls Storefront endpoints run through Shopify's App Proxy and are intended for storefront flows. For backend integrations, use the Admin API instead. ## Key features Read active bundle rules and discount rules for a store so external systems can mirror merchant configuration. Fetch build-a-box configuration by token or product handle and render bundle selectors in custom storefronts. Generate bundle and shipping discounts for eligible build-a-box selections. Record storefront visitor activity for Appstle-powered bundle widgets and customer-facing flows. ## Base URL All Admin API endpoints use: ``` https://bundles-admin.appstle.com ``` Storefront API endpoints are accessed through your store's Shopify App Proxy — no separate base URL is needed. ## HTTP status codes All API responses use standard HTTP status codes. | Code | Meaning | | ----- | ------------------------------------------ | | `200` | Success | | `400` | Bad request — invalid parameters | | `401` | Unauthorized — missing or invalid API key | | `403` | Forbidden — key lacks required permissions | | `404` | Not found | | `429` | Rate limit exceeded | | `500` | Server error | Error responses follow this shape: ```json theme={null} { "error": "Unauthorized", "message": "Invalid API key provided", "status": 401 } ``` ## Next steps Create an API key and learn how to authenticate server-side Bundles API calls. Get an API key and make your first bundle-rule request. Review the main Admin and Storefront endpoint categories. # Partner Integration Framework overview Source: https://developers.appstle.com/bundles/partner-framework-overview How the Appstle Bundles Partner Integration Framework works — one handshake per merchant, a scoped API token, no API plan required, automatic revocation. The Partner Integration Framework lets your product — a page builder, search tool, or review platform — connect to Appstle Bundles on behalf of many merchants. Instead of asking each merchant to create and paste an API key, your app completes a one-time handshake per store and receives a **scoped API token** for it. ## How a connection works You receive a **Partner ID** and **Partner Secret** used only for connection calls. Either from your product's UI, or from **Settings → Partner Connections** in their Appstle dashboard. Connections your app initiates stay pending until the merchant approves them in Appstle. Pending requests expire after 30 days. Appstle delivers a merchant-specific `apst_...` token to your callback. Send it as `X-API-Key` on Admin API calls — exactly like a regular API key. When a merchant disconnects your app — or uninstalls Appstle — the token is revoked immediately. ## Why use it * **No API plan required** — merchants are never billed for partner API usage * **One isolated token per merchant** — no shared credentials, no manual key exchange, individually revocable * **Merchant-controlled** — merchants see, approve, and disconnect partners from their own dashboard * **Automatic cleanup** — access is revoked the moment a merchant disconnects or uninstalls ## Access levels Your app's permission level is set during onboarding: | Permission | What your app can do | | ---------------- | ------------------------------------------------------------------------ | | **Read Only** | View bundle rules, Build-a-Box configurations, and discount rules | | **Read & Write** | Everything above, plus write operations as the Bundles Admin API expands | The Bundles Admin API is read-focused today, so most partners run **Read Only**. ## Connection modes | Mode | Use it when | Your app receives | | ----------------------------- | ------------------------------------------ | --------------------------------------------------- | | **Nonce Handshake** (default) | Your app needs to call Appstle's Admin API | A merchant-scoped `apst_...` API token | | **Simple Token Exchange** | Appstle should push data to *your* API | No Appstle token — Appstle stores a token you issue | ## Get started Email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email to get onboarded. Then follow the [Partner integration guide](/bundles/partner-integration) for the full implementation — endpoints, handshake, callbacks, and testing. # Partner Integration Framework Source: https://developers.appstle.com/bundles/partner-integration Build a zero-configuration integration between your app and Appstle Bundles, with scoped API tokens per merchant. Build a seamless, zero-configuration integration between your app and Appstle Bundles. Once connected, your app gets a scoped API token for each merchant — no manual key exchange needed. **Why become a Partner?** * **Frictionless merchant onboarding** — one-click connect from either dashboard * **No API paywall** — merchants don't need a paid API plan to use your integration * **Scoped tokens** — each merchant gets an isolated API key; revocable at any time * **Automatic cleanup** — when a merchant disconnects or uninstalls, access is revoked instantly ## How it works The Partner Integration Framework uses a secure handshake protocol. Either side — your app or Appstle — can initiate the connection. Both flows end with your app receiving a scoped API token. ```mermaid theme={null} sequenceDiagram autonumber participant P as Partner App participant A as Appstle Bundles rect rgb(240, 248, 255) Note over P,A: Flow A — Partner initiates P->>A: POST /api/partner/{id}/connect
(shop_domain, callback_nonce, secret) A->>P: POST {your_base_url}/appstle/verify
(nonce check) P-->>A: { "verified": true } A-->>P: { "status": "pending_merchant_approval" } Note over P,A: Merchant approves in Appstle dashboard A->>P: POST {your_base_url}/appstle/approved
(access_token) end rect rgb(245, 245, 250) Note over P,A: Flow B — Appstle initiates A->>P: POST {your_base_url}/appstle/connect
(shop_domain, app, callback_url, nonce) P->>A: POST /api/partner/{id}/verify
(shop_domain, callback_nonce, secret) A-->>P: { "access_token": "..." } end ``` **Merchant approval:** When your app initiates a connection (Flow A), the merchant must approve it from their Appstle dashboard before you receive an API token. When the merchant initiates from Appstle's side (Flow B), the connection is approved instantly because the merchant is the one clicking "Connect." ## Getting started ### Step 1: Get onboarded To get started, reach out to the Appstle team at [support@appstle.com](mailto:support@appstle.com) with the information below. Our team will set up your partner account and send you your credentials. #### What you'll need to provide | # | Field | Required? | Description | Example | | - | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- | | 1 | **App / Company Name** | Required | Your app or company name. Displayed to merchants when they browse available partner integrations in the Appstle dashboard. | `BoxBuilder` | | 2 | **Partner ID** | Required | A unique, lowercase slug that identifies your app in API URLs. Use only lowercase letters and hyphens. Once set, this cannot be changed. | `box-builder` | | 3 | **Base URL** | Required | The HTTPS base URL where Appstle will send callback requests (connect, verify, approved). Must be publicly accessible — Appstle will not call HTTP or localhost URLs. | `https://api.boxbuilder.com` | | 4 | **Contact Email** | Required | The email address where we'll send your Partner Secret and any onboarding follow-ups. Use a team email if possible — the secret is shown only once. | `dev-team@boxbuilder.com` | | 5 | **Authentication Mode** | Optional | How your API calls are authenticated. Choose one: **Partner Secret** (simpler — pass secret in a header) or **HMAC-SHA256** (more secure — sign each request). Defaults to Partner Secret if not specified. | `Partner Secret` | | 6 | **Connect Mode** | Optional | How merchant connections are established. Choose one: **Nonce Handshake** (full two-way verification — you receive an Appstle API key) or **Simple Token Exchange** (streamlined — you provide your own token for Appstle to call your API). Defaults to Nonce Handshake if not specified. | `Nonce Handshake` | | 7 | **Custom Endpoint Paths** | Optional | By default, Appstle calls `/appstle/connect` and `/appstle/verify` on your Base URL. If you need different paths (e.g., `/webhooks/appstle/connect`), specify them here. | `/webhooks/appstle/connect` | | 8 | **Sync Path** | Optional | If you want Appstle to push bundle data to your app (e.g., when bundle rules or build-a-box configurations change), provide the path on your server where Appstle should send these payloads. | `/appstle/sync` | | 9 | **App Logo** | Optional | A square logo (PNG or SVG, at least 128×128px) displayed next to your app name in the merchant's Appstle dashboard. If not provided, a placeholder icon is used. | — | Not sure about some of these? Only the first four fields are required to get started. You can always reach out to [support@appstle.com](mailto:support@appstle.com) to change your authentication mode, connect mode, or add a sync path later. **Recommended default for new partners: Simple Token Exchange + Partner Secret.** This is the lowest-friction setup — your app generates a single access token per merchant, hands it to Appstle, and authenticates calls with an `X-Partner-Secret` header. No nonce storage, no HMAC computation, no `/appstle/verify` endpoint to implement. Pick this unless you specifically need Appstle to call your API on behalf of a merchant (use Nonce Handshake) or your security review mandates request signing (use HMAC-SHA256). #### API namespaces — what you call vs. what is internal Three URL namespaces appear in this codebase. **As a third-party partner, you only ever call the first one.** The others exist for Appstle's merchant portal and inter-app integrations and are documented here so the surface area is unambiguous: | Namespace | Who calls it | Purpose | | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/partner/...` | **You** (the partner) | Connect, verify, disconnect, status. Authenticated with your Partner Secret or HMAC. | | `/api/integrations/partner/...` | Appstle merchant-portal UI | Drives the merchant-facing "Connect / Disconnect" buttons in the Appstle dashboard. Session-authenticated; not part of the public partner API. | | `/api/integrations/callback/...` | Other Appstle apps | Receiver-side callbacks for app-to-app integrations between Appstle products. Not used by third-party partners. | If you see an example referring to `/api/integrations/...`, it's an internal Appstle flow and doesn't apply to your integration. #### What you'll receive Once onboarded, you'll receive three values: | Credential | Example | Description | | ------------------ | ----------------------------------- | ----------------------------------------------------------------------------- | | **Partner ID** | `box-builder` | Your unique identifier, as requested. Becomes part of the API URL. | | **Partner Secret** | `xK9mQ2vL...` (48 chars) | A secret key used to authenticate your API calls. Treat this like a password. | | **Base URL** | `https://bundles-admin.appstle.com` | Appstle's API base URL. Same for all partners. | Your Partner Secret is shown **only once** during onboarding. Copy it immediately and store it in a secure location (environment variable, secrets manager, etc.). If you lose it, contact Appstle to rotate it — the old secret will be invalidated immediately. Store your credentials as environment variables: ```bash .env theme={null} # never commit this file APPSTLE_PARTNER_ID=box-builder APPSTLE_PARTNER_SECRET=xK9mQ2vLa8nR3pY... # if using Partner Secret auth APPSTLE_HMAC_KEY=your-hmac-key-here # if using HMAC-SHA256 auth APPSTLE_BASE_URL=https://bundles-admin.appstle.com ``` ### Step 1b: Choose your authentication mode Appstle supports two ways to authenticate partner API calls. Your auth mode is configured during onboarding. #### Option A: Partner Secret (default) The simplest approach. Pass your secret in a header with every request: ``` X-Partner-Secret: your-partner-secret ``` That's it. No computation needed. Good for getting started quickly. #### Option B: HMAC-SHA256 A more secure approach where requests are signed with a shared HMAC key. Instead of sending the secret directly, you compute a signature over the request body. Headers required: ``` X-Partner-Timestamp: 1709856000 X-Partner-Signature: 5a3c1f2e9b8d7a6c... ``` How to compute the signature: 1. Get the current Unix timestamp (seconds, not milliseconds) 2. Concatenate the timestamp and the raw JSON request body: `timestamp + body` 3. Compute HMAC-SHA256 of that string using your HMAC key 4. Send the hex-encoded result in `X-Partner-Signature` **Timestamp validation:** Appstle rejects requests where the timestamp is more than **5 minutes** from the server's current time. Make sure your server clock is synced (NTP). ```javascript Node.js theme={null} const crypto = require('crypto'); function signRequest(body, hmacKey) { const timestamp = Math.floor(Date.now() / 1000).toString(); const data = timestamp + body; const signature = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', }; } // Usage const body = JSON.stringify({ shop_domain: 'cool-store.myshopify.com' }); const headers = signRequest(body, process.env.APPSTLE_HMAC_KEY); ``` ```python Python theme={null} import hmac import hashlib import time import json def sign_request(body: str, hmac_key: str) -> dict: timestamp = str(int(time.time())) data = timestamp + body signature = hmac.new( hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256, ).hexdigest() return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', } # Usage body = json.dumps({"shop_domain": "cool-store.myshopify.com"}) headers = sign_request(body, os.environ["APPSTLE_HMAC_KEY"]) ``` ```bash curl theme={null} # Compute signature: HMAC-SHA256(timestamp + body, key) TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://bundles-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Which should I choose?** * **Partner Secret** — simpler to implement, fine for most integrations * **HMAC-SHA256** — better security (secret never sent over the wire), recommended for high-volume or security-sensitive integrations Both are equally supported. You can switch modes later by contacting Appstle. ### Step 1c: Choose your connect mode Appstle supports two ways to establish merchant connections. Your connect mode is configured during onboarding. #### Option A: Nonce Handshake (default) The full two-way verification flow described in this guide. Both sides verify each other using a one-time nonce. After the handshake, your app receives an **Appstle API key** (`apst_...`) to call Appstle's External API. **Best for:** Partners who want to read bundle configuration in Appstle (bundle rules, build-a-box configurations, discount rules, etc.) #### Option B: Simple Token Exchange A streamlined flow where your app sends its own access token to Appstle (or Appstle calls your connect endpoint and you return one). No nonce, no verify endpoint needed. Appstle stores your token and uses it to call **your** API when needed. **Best for:** Partners where Appstle needs to call the partner's API (e.g., syncing data to the partner's platform), rather than the partner calling Appstle's API. **Key difference from Nonce Handshake:** In Simple Token Exchange, your app provides its own access token to Appstle. Appstle stores this token and uses it to push data to your API (via your `sync_path` — see [Data sync](#data-sync-push-model) below). Your app does **not** receive an Appstle API key in this mode. **Need both directions?** If you need to both push data to Appstle AND have Appstle push data to you, use the Nonce Handshake mode and provide a `sync_path` during onboarding. Contact [support@appstle.com](mailto:support@appstle.com) to discuss your use case. **How Simple Token Exchange works:** *Partner-initiated:* ```bash theme={null} curl -X POST "https://bundles-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: 1709856000" \ -H "X-Partner-Signature: 5a3c1f2e..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "access_token": "your-apps-token-for-this-merchant" }' ``` Response: ```json theme={null} { "status": "pending_merchant_approval" } ``` Your `access_token` is stored securely but will not be activated until the merchant approves the connection from their Appstle dashboard. Once approved, Appstle calls your `/appstle/approved` endpoint to confirm (see [Handling the approval callback](#handling-the-approval-callback)). *Appstle-initiated:* Appstle calls your `/appstle/connect` endpoint with `{ "shop_domain": "..." }`. Your app responds with: ```json theme={null} { "success": true, "access_token": "your-apps-token-for-this-merchant" } ``` With Simple Token Exchange, your app does NOT receive an Appstle API key. If you also need to call Appstle's External API, use the Nonce Handshake mode instead. ### Step 2: Understand the callback nonce This section applies to Nonce Handshake mode only. If you're using Simple Token Exchange, skip to [Step 4](#step-4-implement-the-connect-flow-your-dashboard). Before implementing, you need to understand the **callback nonce** — it's the core security mechanism of the handshake. #### What is a callback nonce? A **nonce** (number used once) is a random, single-use string that proves both sides of the connection are who they claim to be. It prevents replay attacks and ensures the handshake can't be forged. #### Requirements | Requirement | Detail | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Length** | At least 32 bytes (64 hex characters) | | **Randomness** | Must be **cryptographically random** — do NOT use `Math.random()`, `rand()`, timestamps, or UUIDs | | **Single-use** | Each nonce must be used exactly once, then deleted | | **Expiry** | Nonces expire after **5 minutes** on Appstle's side. Your storage should also expire them. | | **Storage** | Store temporarily with a TTL. Redis, DynamoDB, or any key-value store with expiry works. Database with a cleanup job is also fine. | #### How to generate a nonce Use your language's cryptographically secure random number generator. ```javascript Node.js theme={null} const crypto = require('crypto'); // Generate a 32-byte (64 hex character) cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```python Python theme={null} import secrets # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = secrets.token_hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```ruby Ruby theme={null} require 'securerandom' # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = SecureRandom.hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```php PHP theme={null} // Generate a 32-byte (64 hex character) cryptographically random nonce $nonce = bin2hex(random_bytes(32)); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```java Java theme={null} import java.security.SecureRandom; SecureRandom secureRandom = new SecureRandom(); byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); StringBuilder sb = new StringBuilder(64); for (byte b : bytes) { sb.append(String.format("%02x", b)); } String nonce = sb.toString(); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```go Go theme={null} import ( "crypto/rand" "encoding/hex" ) bytes := make([]byte, 32) rand.Read(bytes) nonce := hex.EncodeToString(bytes) // Result: "a1b2c3d4e5f6...64 characters total" ``` **Common mistakes:** * `Math.random().toString(36)` — not cryptographically random, predictable * `uuid.v4()` — UUIDs are not designed as security tokens (some implementations use weak RNG) * `Date.now().toString()` — trivially guessable * Reusing nonces across multiple connect attempts Always use your language's `crypto` / `secrets` / `SecureRandom` module. #### How to store a nonce Store the nonce temporarily, keyed by shop domain, with a 5-minute expiry. Delete it after verification. ```javascript Node.js + Redis theme={null} const Redis = require('ioredis'); const redis = new Redis(); // Store nonce with 5-minute TTL async function storeNonce(shopDomain, nonce) { const key = `appstle:nonce:${shopDomain}`; await redis.set(key, nonce, 'EX', 300); // 300 seconds = 5 minutes } // Retrieve and delete nonce (single atomic operation) async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const storedNonce = await redis.get(key); if (!storedNonce || storedNonce !== nonceToCheck) { return false; } await redis.del(key); return true; } ``` ```python Python + database theme={null} from datetime import datetime, timedelta from your_app.models import PartnerNonce # your ORM model def store_nonce(shop_domain: str, nonce: str): # Delete any existing nonce for this shop (prevent duplicates) PartnerNonce.objects.filter(shop_domain=shop_domain).delete() PartnerNonce.objects.create( shop_domain=shop_domain, nonce=nonce, expires_at=datetime.utcnow() + timedelta(minutes=5), ) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: try: record = PartnerNonce.objects.get( shop_domain=shop_domain, nonce=nonce_to_check, expires_at__gt=datetime.utcnow(), # not expired ) record.delete() return True except PartnerNonce.DoesNotExist: return False ``` ### Step 3: Implement your endpoints Your app must expose two HTTP endpoints that Appstle calls during the connection handshake. The paths default to `/appstle/connect` and `/appstle/verify` but can be customized during onboarding. **Both endpoints must:** * Accept `POST` requests with a JSON body * Return JSON responses * Be accessible over **HTTPS** (Appstle will not call HTTP endpoints) * Respond within **10 seconds** (or the request will time out) * **Be idempotent.** Appstle may retry a callback on transient failure, and a merchant flipping connect/disconnect repeatedly will exercise the same endpoint with the same `(shop_domain, partnerId)` pair. Treat every call as an upsert keyed by `(shop_domain, partnerId)` — never blindly insert. The same rule applies to your `/appstle/approved` and `/appstle/disconnect` endpoints described later. #### Endpoint 1: `POST /appstle/connect` **When is this called?** Appstle calls this when a merchant initiates the connection from **Appstle's dashboard** (Flow B). **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "app": "bundles", "callback_url": "https://bundles-admin.appstle.com/api/partner/your-partner-id/verify", "callback_nonce": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" } ``` | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `shop_domain` | string | The merchant's Shopify domain (e.g. `cool-store.myshopify.com`) | | `app` | string | Always `"bundles"` — identifies which Appstle app is connecting | | `callback_url` | string | The exact URL your app must call to complete the handshake. **The `{partnerId}` embedded in this URL is *Appstle's* identifier for this Appstle app on your side, not your Partner ID.** Use the URL verbatim — don't parse or substitute the segment. | | `callback_nonce` | string | A one-time-use token generated by Appstle. **Expires in 5 minutes.** | **What should your app do?** 1. **Validate the shop** — check that this `shop_domain` exists in your system. If you don't recognize the shop, return an error. 2. **Store the nonce and callback URL** — save `callback_nonce` and `callback_url` associated with this `shop_domain`. You'll need them to complete the handshake. 3. **Call back to Appstle** — either immediately (auto-approve) or after merchant confirmation, call the `callback_url` to complete the connection. See [Completing the handshake](#completing-the-handshake-flow-b) below. 4. **Return a success response** — any `2xx` status code tells Appstle the request was received. ```javascript Node.js (Express) theme={null} const express = require('express'); const axios = require('axios'); const router = express.Router(); router.post('/appstle/connect', async (req, res) => { const { shop_domain, app, callback_url, callback_nonce } = req.body; // 1. Validate: does this shop exist in your system? const shop = await db.shops.findOne({ domain: shop_domain }); if (!shop) { return res.status(400).json({ error: 'Shop not found in our system' }); } // 2. Store the nonce and callback URL for this shop await db.pendingConnections.upsert({ shopDomain: shop_domain, callbackUrl: callback_url, callbackNonce: callback_nonce, createdAt: new Date(), expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes }); // 3. Option A: Auto-approve (call back immediately) try { const response = await axios.post(callback_url, { shop_domain: shop_domain, callback_nonce: callback_nonce, }, { headers: { 'X-Partner-Secret': process.env.APPSTLE_PARTNER_SECRET, 'Content-Type': 'application/json', }, }); if (response.data.verified && response.data.access_token) { // 4. Store the access token for this merchant await db.appstleTokens.upsert({ shopDomain: shop_domain, accessToken: response.data.access_token, connectedAt: new Date(), }); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } // 5. Return success to Appstle res.json({ success: true }); }); ``` #### Endpoint 2: `POST /appstle/verify` **When is this called?** Appstle calls this when a merchant initiates the connection from **your app's dashboard** (Flow A). Appstle is asking your app: "Did you actually send this nonce?" **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f6...the-nonce-you-generated" } ``` | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------- | | `shop_domain` | string | The merchant's Shopify domain | | `callback_nonce` | string | The nonce your app originally sent in the `/connect` call | **What should your app do?** 1. Look up the stored nonce for this `shop_domain` 2. Compare the `callback_nonce` from the request against your stored nonce 3. If they match: delete the stored nonce (it's single-use) and return `{ "verified": true }` 4. If they don't match: return `{ "verified": false }` ```javascript Node.js (Express) theme={null} router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; // 1. Look up the stored nonce for this shop const isValid = await verifyAndDeleteNonce(shop_domain, callback_nonce); // 2. Return the result res.json({ verified: isValid }); }); ``` ### Step 4: Implement the connect flow (your dashboard) Now build the merchant-facing "Connect Appstle Bundles" button in your app's dashboard. #### Partner-initiated connect (Flow A) — step by step This is the flow where the merchant clicks "Connect Appstle" in **your** dashboard. The merchant initiates the connection from inside your app's UI. Store it keyed by `shop_domain` with a 5-minute TTL. Send the `shop_domain`, the `callback_nonce`, and your Partner Secret. Appstle also confirms the shop has Appstle Bundles installed. Payload: the `shop_domain` and the same `callback_nonce`. Confirm it matches, delete it, return `{ "verified": true }`. The connection is pending — no access token has been issued yet. They open **Settings → Partner Connections** and click **Approve**. The token is POSTed to YOUR `/appstle/approved` endpoint. Show "Connected!" to the merchant. You're done. **Full implementation (Node.js):** ```javascript theme={null} const crypto = require('crypto'); const axios = require('axios'); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL; // https://bundles-admin.appstle.com // Called when merchant clicks "Connect Appstle" in your dashboard async function connectToAppstle(shopDomain) { // Step 2: Generate a cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Store it so your /appstle/verify endpoint can look it up later await storeNonce(shopDomain, nonce); // see nonce storage examples above // Step 3: Call Appstle's partner connect endpoint const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json', }, } ); // Steps 4-6 happen automatically (Appstle calls your /appstle/verify) // Step 7: Appstle returns pending status — token is NOT delivered yet const { status } = response.data; if (status === 'pending_merchant_approval') { // The merchant needs to approve in their Appstle dashboard. // Once approved, Appstle will POST the token to your /appstle/approved endpoint. await markConnectionPending(shopDomain); return { pending: true }; } throw new Error('Connection failed'); } ``` **curl equivalent:** ```bash theme={null} curl -X POST "https://bundles-admin.appstle.com/api/partner/box-builder/connect" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890" }' ``` **Success response:** ```json theme={null} { "status": "pending_merchant_approval" } ``` **What happens next?** The merchant will see a "Pending Request" in their Appstle dashboard under **Settings → Partner Connections**. (This menu appears automatically once a partner initiates a connection request — it is not visible before any partner has connected.) When they click "Approve," Appstle creates a scoped API key and delivers it to your `/appstle/approved` endpoint (see [Handling the approval callback](#handling-the-approval-callback) below). Pending requests expire after **30 days** if not acted on. **Deep link to approval screen:** You can redirect the merchant directly to the approval screen to minimize friction: ``` https://admin.shopify.com/store/{shop-handle}/apps/appstle-bundles/settings/partner-connections ``` Replace `{shop-handle}` with the merchant's store handle (the part before `.myshopify.com`). This takes them straight to the pending connection for one-click approval. You can trigger this redirect in your UI immediately after receiving the `pending_merchant_approval` response. #### Appstle-initiated connect (Flow B) — step by step This is the flow where the merchant clicks "Connect" in **Appstle's** dashboard. The connection is initiated from inside Appstle, not your UI. Internal Appstle call — your app is not involved yet. Payload: `shop_domain`, `app`, `callback_url`, and `callback_nonce`. Persist them keyed by `shop_domain` for the verify step. Send `shop_domain`, `callback_nonce`, and your Partner Secret. The `access_token` is returned in the response body. Connection complete. ##### Completing the handshake (Flow B) After your `/appstle/connect` endpoint receives the nonce and callback URL, your app completes the connection by calling Appstle's verify endpoint: ```bash theme={null} curl -X POST "https://bundles-admin.appstle.com/api/partner/box-builder/verify" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "the-nonce-appstle-sent-in-the-connect-call" }' ``` **Success response:** ```json theme={null} { "verified": true, "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Failed response (nonce expired or mismatched):** ```json theme={null} { "verified": false } ``` You must call the verify endpoint within **5 minutes** of receiving the nonce. After that, the nonce expires and the merchant will need to try again. ## Using the API token After a successful connection, your app has an `access_token` (prefixed with `apst_`). For **Appstle-initiated connections** (Flow B), the token is returned immediately in the verify response. For **partner-initiated connections** (Flow A), the token is delivered asynchronously to your `/appstle/approved` endpoint after the merchant approves (see [Handling the approval callback](#handling-the-approval-callback) below). Use this token exactly like a merchant API key — pass it in the `X-API-Key` header: ```bash theme={null} curl -X GET \ "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=cool-store.myshopify.com" \ -H "X-API-Key: apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" ``` ### Token properties | Property | Detail | | -------------- | ---------------------------------------------------------------------------------------------- | | **Format** | Starts with `apst_` followed by 40 alphanumeric characters | | **Scope** | One token per merchant per partner | | **Permission** | `READ_ONLY` or `READ_WRITE` (set during partner onboarding) | | **Billing** | Partner tokens **bypass the paid API plan** — merchants are never billed for partner API usage | | **Revocation** | Revoked instantly when the merchant disconnects or uninstalls Appstle | | **Expiry** | Tokens do not expire on their own. They remain valid until explicitly revoked. | ### Available endpoints Partner tokens grant access to the same [External API endpoints](/bundles/integration-guide) as merchant API keys: * **Bundle rules** — `GET /api/external/bundle-rules` * **Build-a-box rules** — `GET /api/external/bundle-rules/bab` * **Discount rules** — `GET /api/external/bundle-rules/discount` Every Bundles external endpoint takes a `shop` query parameter (e.g. `?shop=cool-store.myshopify.com`) and is **read-only**, so a `READ_ONLY` token can call all of them. `READ_WRITE` is reserved for future write endpoints. See the full [Integration guide](/bundles/integration-guide) for complete endpoint documentation. ## Data sync (push model) Some integrations work best when **Appstle pushes data to your app**, rather than your app pulling from Appstle's API. For example, a search platform might need Appstle to push bundle data so it can be indexed alongside other store data. ### How it works During onboarding, you can configure a `sync_path` on your server (e.g., `/appstle/sync`). When bundle events occur (bundle rules or build-a-box configurations created, updated, or removed), Appstle calls your endpoint with the relevant data. | Config Field | Example | Description | | ----------------- | --------------------- | ------------------------------------------------ | | `sync_path` | `/appstle/sync` | Your endpoint where Appstle pushes bundle data | | `disconnect_path` | `/appstle/disconnect` | Your endpoint called when a merchant disconnects | ### Authentication When Appstle calls your endpoints, it authenticates using the auth mode configured for your partner: * **Partner Secret mode:** No additional headers (your endpoints are responsible for validating the source — consider IP allowlisting) * **HMAC-SHA256 mode (recommended):** Appstle signs every request with `X-Partner-Timestamp` and `X-Partner-Signature` headers. Your app should verify the HMAC signature to confirm the request came from Appstle. **Verifying incoming HMAC signatures (Node.js):** ```javascript theme={null} const crypto = require('crypto'); function verifyAppstleSignature(req, hmacKey) { const timestamp = req.headers['x-partner-timestamp']; const signature = req.headers['x-partner-signature']; if (!timestamp || !signature) return false; // Reject if timestamp is more than 5 minutes old const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) return false; // Compute expected signature const data = timestamp + req.rawBody; // make sure you capture raw body const expected = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); // Constant-time comparison return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ### Pull vs push — which model do I need? | Model | Connect Mode | Your App Calls Appstle? | Appstle Calls Your App? | Use Case | | ------------------ | ---------------------------- | ------------------------- | --------------------------------- | ------------------------------------------------- | | **Pull** (default) | Nonce Handshake | Yes (via `apst_` API key) | No | Helpdesks, CRMs reading bundle data on demand | | **Push** | Simple Token Exchange | No | Yes (via your token + sync\_path) | Search platforms, analytics tools that index data | | **Bidirectional** | Nonce Handshake + sync\_path | Yes | Yes | Full two-way integrations | ## Disconnecting ### Merchant disconnects from Appstle Merchants can disconnect your integration anytime from **Appstle Dashboard → Settings → Partner Connections**. When they do: * Your API token for that merchant is **revoked immediately** * Subsequent API calls will return `401 Unauthorized` * **Appstle sends a disconnect webhook** to your app (if you configured a `disconnect_path` during onboarding — see below) * Your app should handle this gracefully and show a "Reconnect" option **Best practice:** In your API client, check for `401` responses and update your UI to show the connection as disconnected: ```javascript theme={null} async function callAppstleApi(shopDomain, endpoint) { const token = await getAccessToken(shopDomain); try { const response = await axios.get(`${APPSTLE_BASE}${endpoint}`, { headers: { 'X-API-Key': token }, }); return response.data; } catch (err) { if (err.response?.status === 401) { // Token was revoked — merchant disconnected await markAsDisconnected(shopDomain); throw new Error('Appstle connection was revoked. Merchant needs to reconnect.'); } throw err; } } ``` ### Disconnect webhook (first-class endpoint) Configure a `disconnect_path` during onboarding (defaults to `/appstle/disconnect`). Appstle calls this whenever a merchant disconnects — from the Appstle dashboard, from your app, or by uninstalling Appstle entirely. **You should implement this endpoint for every integration** — it is the only reliable signal that the merchant has revoked access on Appstle's side. Polling `401` responses as a fallback works but lags behind. **Request body from Appstle:** ```json theme={null} { "shop_domain": "cool-store.myshopify.com" } ``` If your partner uses HMAC-SHA256 auth, the webhook includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. **Your endpoint must:** 1. **Look up the connection without filtering on status.** Don't `WHERE status = 'active'` — if the merchant rapid-clicks disconnect twice, the second call may arrive when the row is already inactive. Find by `(shop_domain, partnerId)` only. 2. **Revoke the Appstle access token idempotently.** If the token is already revoked or absent, return success — don't error. Revocation must be safe to call repeatedly. 3. **Mark the local connection inactive.** Clear or null out the stored Appstle token so subsequent API calls don't try to use it. 4. **Return `2xx` even when there was nothing to do.** A no-op disconnect is a successful disconnect from Appstle's perspective. ```javascript Node.js (Express) theme={null} router.post('/appstle/disconnect', async (req, res) => { const { shop_domain } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } // 1. Status-agnostic lookup — don't filter on .where({ status: 'active' }) const connection = await db.connections.findOne({ shopDomain: shop_domain }); if (connection) { // 2. Idempotent token revoke — clearing a null token is a no-op await db.appstleTokens.delete({ shopDomain: shop_domain }); // 3. Mark inactive (upsert-style — safe if already inactive) await db.connections.update( { shopDomain: shop_domain }, { status: 'disconnected', disconnectedAt: new Date() } ); } // 4. Always 2xx — even if nothing was found res.json({ success: true }); }); ``` ```python Python (Flask) theme={null} @app.route("/appstle/disconnect", methods=["POST"]) def appstle_disconnect(): shop_domain = request.json["shop_domain"] # 1. Find without filtering on status connection = Connection.query.filter_by(shop_domain=shop_domain).first() if connection: # 2. Idempotent token revoke AppstleToken.query.filter_by(shop_domain=shop_domain).delete() # 3. Mark inactive (upsert semantics) connection.status = "disconnected" connection.disconnected_at = datetime.utcnow() db.session.commit() # 4. Always 2xx return jsonify({"success": True}) ``` This is a best-effort notification — your app should also handle `401` responses from the Appstle API as a fallback signal that the connection was revoked. ### Partner disconnects programmatically Your app can disconnect a merchant using your partner authentication (Partner Secret or HMAC-SHA256): ```bash Partner Secret theme={null} curl -X POST "https://bundles-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Secret: YOUR_PARTNER_SECRET" \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com" }' ``` ```bash HMAC-SHA256 theme={null} TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://bundles-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Response:** ```json theme={null} { "success": true } ``` ### Check connection status `GET /api/partner/{partnerId}/status?shop_domain=...` is the **authoritative source of truth** for whether a merchant is connected. If your UI shows a "Connected" badge, derive it from this endpoint — not from whether you happen to have a stored API key locally. **Why this matters:** Older integrations sometimes inferred "connected" from the presence of a per-app API key column in their own database. That column is now a deprecated fallback — it can be stale (key revoked on Appstle's side, your row never updated) and it can't represent `pending_merchant_approval` or `rejected`. Always call `/status` before showing connection state to the merchant or making business decisions based on it. Partners can authenticate with their Partner Secret or HMAC signature: ```bash theme={null} # With Partner Secret curl -X GET "https://bundles-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Secret: your-partner-secret" # With HMAC curl -X GET "https://bundles-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" ``` **Response (active connection):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "active", "connected_at": "2026-03-07T20:30:00Z" } ``` **Response (pending merchant approval):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "pending_merchant_approval" } ``` **All possible status values:** | Status | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | Connected and working — API token is valid | | `pending_merchant_approval` | Partner-initiated connect is awaiting merchant approval | | `rejected` | Merchant rejected the connection request | | `expired` | A pending request expired (30-day window) without merchant action — partner must initiate a new connection | | `not_connected` | No connection record exists for this partner + shop, or the connection was previously terminated (by merchant, by partner, or by uninstall). In both cases the token is revoked and your app would need to initiate a new connection. | Use the status endpoint to poll for approval if your app doesn't implement the `/appstle/approved` callback. Poll every 30–60 seconds after initiating a connect. Once the status changes from `pending_merchant_approval` to `active`, your token has been delivered via the approval callback (or you can request it again). ## Handling the approval callback When a merchant approves a partner-initiated connection, Appstle delivers the API token by calling an endpoint on your server. This applies to **partner-initiated connections only** — Appstle-initiated connections (Flow B) return the token immediately. ### Endpoint: `POST /appstle/approved` The path defaults to `/appstle/approved` but can be customized during onboarding (configured as `approval_callback_path`). Your `/appstle/approved` endpoint **must accept unauthenticated POST requests** from Appstle's servers. Do not put authentication middleware (e.g., JWT validation, API key checks) on this endpoint — Appstle will not send your app's auth credentials when calling this callback. If you need to verify the request is from Appstle, use HMAC-SHA256 authentication mode — when enabled, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) you can verify. **Request body from Appstle (Nonce Handshake mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Request body from Appstle (Simple Token Exchange mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "status": "approved" } ``` In Simple Token Exchange mode, your app already provided its own token during the connect call. The approval callback simply confirms the connection is now active — Appstle will start using your token for API calls. **Expected response:** Return any `2xx` status code with a JSON body (e.g., `{ "success": true }`). If your endpoint returns a non-2xx status (e.g., `401 Unauthorized`), the connection is still approved on Appstle's side, but your app won't know — see [What if the callback fails?](#what-if-the-callback-fails) below. If your partner uses HMAC-SHA256 auth, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. ```javascript Node.js theme={null} router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } if (access_token) { // Nonce Handshake mode — store the Appstle API token await saveToken(shop_domain, access_token); console.log(`Connection approved for ${shop_domain} — token received`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active await markConnectionActive(shop_domain); console.log(`Connection approved for ${shop_domain} — our token is now active`); } res.json({ success: true }); }); ``` ```python Python theme={null} @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active mark_connection_active(shop_domain) return jsonify({"success": True}) ``` ### What if the callback fails? If your endpoint is unreachable or returns an error, the connection is still approved on Appstle's side. The API token exists and is valid. Your app can: 1. **Poll the status endpoint** — check `GET /api/partner/{id}/status?shop_domain=...` until the status is `active` 2. **Retry from Appstle's side** — currently, Appstle does not automatically retry the callback. Contact support if you need the token re-delivered. ### What if the merchant rejects? If the merchant clicks "Reject," the connection status changes to `rejected` and Appstle notifies your app via the disconnect webhook (if configured). Your app should handle this gracefully — show the merchant that the connection was not approved. ## Error handling All partner endpoints return structured error responses: ```json theme={null} { "type": "https://bundles-admin.appstle.com/problem", "title": "Bad Request", "status": 400, "detail": "UserGeneratedError:Active connection already exists. Disconnect first.", "errorKey": "ALREADY_CONNECTED" } ``` ### Error codes | Error Code | HTTP Status | When It Happens | What To Do | | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PARTNER_NOT_FOUND` | 400 | Your Partner ID is wrong, or the partner has been deactivated | Double-check your Partner ID. Contact Appstle if unexpected. | | `TOKEN_INVALID` | 400 | Auth failed: `X-Partner-Secret` is wrong, or HMAC signature is invalid, or timestamp is >5 min off | Verify your secret or HMAC key. Check for trailing whitespace. For HMAC: ensure server clock is synced (NTP) and you're signing `timestamp + body` exactly. | | `SHOP_NOT_FOUND` | 400 | The shop doesn't have Appstle Bundles installed | Tell the merchant to install Appstle Bundles first. | | `ALREADY_CONNECTED` | 400 | An active connection already exists for this partner + shop | Call disconnect first, then reconnect. Or skip — you're already connected. | | `VERIFICATION_FAILED` | 400 | Nonce didn't match, expired (>5 min), or your `/verify` endpoint returned `false` | Generate a fresh nonce and try again. Check your nonce storage logic. | | `NOT_CONNECTED` | 400 | Trying to disconnect or check status, but no active connection exists | The merchant may have already disconnected from their side. | | `PARTNER_UNREACHABLE` | 400 | Appstle couldn't reach your `/appstle/connect` or `/appstle/verify` endpoint | Check your endpoint URL is correct, HTTPS, and publicly accessible. Check your server logs. | | `UNEXPECTED_ROLLBACK` | 500 | Your endpoint returned success, but Appstle's transaction was silently rolled back. Manifests in logs as `UnexpectedRollbackException` / `Transaction silently rolled back because it has been marked as rollback-only`. | Common footgun for partners running on transactional frameworks: an inner write throws and gets caught by your handler, but the surrounding transaction has already been marked rollback-only — so the outer commit fails with no visible error from your business logic. Fix is in your code: either let the inner exception propagate, or perform the write in a fresh inner transaction. Don't swallow exceptions inside a transactional boundary. | #### Idempotency requirements — recap The integration framework relies on partners treating callbacks as **at-least-once**. Concretely: * **Connect / approval callbacks:** upsert by `(shop_domain, partnerId)`. Two `/appstle/approved` calls for the same shop must produce the same end state, not two rows. * **Disconnect callback:** find the connection without filtering on status; revoke tokens idempotently; return `2xx` even when there is nothing to do. * **Status reads:** safe by definition — no side effects. If your code is built on "this only ever fires once", expect bugs the first time the merchant flips connect/disconnect quickly or the first time a network blip triggers an Appstle retry. ## Security checklist Before going live, verify all of these: * [ ] **Partner Secret / HMAC Key** is stored in environment variables or a secrets manager — not hardcoded in source code * [ ] **Nonces** are generated using a cryptographically secure random generator (`crypto.randomBytes`, `secrets.token_hex`, `SecureRandom`, etc.) * [ ] **Nonces** are stored with a TTL (≤ 5 minutes) and deleted after verification * [ ] **Nonces** are compared using a constant-time comparison to prevent timing attacks (most frameworks do this by default for string equality) * [ ] **Endpoints** are served over **HTTPS** — Appstle will not call HTTP endpoints * [ ] **`shop_domain`** is validated in your `/appstle/connect` and `/appstle/verify` endpoints — reject domains you don't recognize * [ ] **Access tokens** are stored encrypted at rest (or in a secrets manager) * [ ] **401 responses** are handled gracefully — show a "Reconnect" option, don't break silently * [ ] **Error responses** from Appstle are logged for debugging * [ ] **(HMAC only)** Server clock is synced via NTP — timestamps more than 5 minutes off will be rejected * [ ] **(If using disconnect webhook)** Your `/appstle/disconnect` endpoint cleans up stored tokens and marks the connection as inactive ## Complete example: partner-initiated flow ```javascript Node.js theme={null} // appstle-partner.js const express = require('express'); const crypto = require('crypto'); const axios = require('axios'); const Redis = require('ioredis'); const router = express.Router(); const redis = new Redis(process.env.REDIS_URL); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL || 'https://bundles-admin.appstle.com'; const NONCE_TTL = 300; // 5 minutes in seconds // ────────────────────────────────────────────── // Nonce helpers // ────────────────────────────────────────────── async function storeNonce(shopDomain, nonce) { await redis.set(`appstle:nonce:${shopDomain}`, nonce, 'EX', NONCE_TTL); } async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const stored = await redis.get(key); if (!stored || stored !== nonceToCheck) return false; await redis.del(key); return true; } // ────────────────────────────────────────────── // Token storage (use your database in production) // ────────────────────────────────────────────── async function saveToken(shopDomain, accessToken) { // In production: encrypt the token before storing await redis.set(`appstle:token:${shopDomain}`, accessToken); } async function getToken(shopDomain) { return redis.get(`appstle:token:${shopDomain}`); } // ────────────────────────────────────────────── // Flow A: Partner-initiated connect // Called when merchant clicks "Connect Appstle" in YOUR dashboard // ────────────────────────────────────────────── router.post('/connect-appstle', async (req, res) => { const { shopDomain } = req.body; try { // 1. Generate nonce const nonce = crypto.randomBytes(32).toString('hex'); await storeNonce(shopDomain, nonce); // 2. Call Appstle const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' } } ); // 3. Connection is now pending merchant approval if (response.data.status === 'pending_merchant_approval') { // Mark as pending in your system — show the merchant a "waiting for approval" state await redis.set(`appstle:pending:${shopDomain}`, 'true'); return res.json({ pending: true, message: 'Waiting for merchant to approve in Appstle dashboard' }); } res.status(400).json({ error: 'Connection failed' }); } catch (err) { const detail = err.response?.data?.detail || err.message; console.error('Appstle connect failed:', detail); res.status(400).json({ error: detail }); } }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/verify // Called BY Appstle during Flow A to verify your nonce // ────────────────────────────────────────────── router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; const verified = await verifyAndDeleteNonce(shop_domain, callback_nonce); res.json({ verified }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/approved // Called BY Appstle when merchant approves a partner-initiated connection // ────────────────────────────────────────────── router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; if (access_token) { // Nonce Handshake mode — Appstle is delivering our API token await saveToken(shop_domain, access_token); await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Token received for ${shop_domain}`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active on Appstle's side await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Our token is now active for ${shop_domain}`); } res.json({ success: true }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/connect // Called BY Appstle during Flow B (Appstle-initiated) // ────────────────────────────────────────────── router.post('/appstle/connect', async (req, res) => { const { shop_domain, callback_url, callback_nonce } = req.body; // Verify the shop exists in your system // const shop = await db.shops.findOne({ domain: shop_domain }); // if (!shop) return res.status(400).json({ error: 'Unknown shop' }); // Auto-approve: immediately call back to complete the handshake // (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try { const response = await axios.post(callback_url, { shop_domain, callback_nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' }, }); if (response.data.verified && response.data.access_token) { await saveToken(shop_domain, response.data.access_token); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } res.json({ success: true }); }); module.exports = router; ``` ```python Python theme={null} # appstle_partner.py import os import secrets import redis import requests from flask import Flask, request, jsonify app = Flask(__name__) r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379")) PARTNER_ID = os.environ["APPSTLE_PARTNER_ID"] PARTNER_SECRET = os.environ["APPSTLE_PARTNER_SECRET"] APPSTLE_BASE = os.environ.get("APPSTLE_BASE_URL", "https://bundles-admin.appstle.com") NONCE_TTL = 300 # 5 minutes def store_nonce(shop_domain, nonce): r.set(f"appstle:nonce:{shop_domain}", nonce, ex=NONCE_TTL) def verify_and_delete_nonce(shop_domain, nonce_to_check): key = f"appstle:nonce:{shop_domain}" stored = r.get(key) if not stored or stored.decode() != nonce_to_check: return False r.delete(key) return True def save_token(shop_domain, access_token): r.set(f"appstle:token:{shop_domain}", access_token) # ── Flow A: Partner-initiated connect ── @app.route("/connect-appstle", methods=["POST"]) def connect_appstle(): shop_domain = request.json["shopDomain"] # 1. Generate nonce nonce = secrets.token_hex(32) store_nonce(shop_domain, nonce) # 2. Call Appstle resp = requests.post( f"{APPSTLE_BASE}/api/partner/{PARTNER_ID}/connect", json={"shop_domain": shop_domain, "callback_nonce": nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) resp.raise_for_status() data = resp.json() # 3. Connection is pending merchant approval if data.get("status") == "pending_merchant_approval": r.set(f"appstle:pending:{shop_domain}", "true") return jsonify({"pending": True, "message": "Waiting for merchant to approve in Appstle dashboard"}) return jsonify({"error": "Connection failed"}), 400 # ── Endpoint: POST /appstle/verify (called BY Appstle during Flow A) ── @app.route("/appstle/verify", methods=["POST"]) def appstle_verify(): body = request.json verified = verify_and_delete_nonce(body["shop_domain"], body["callback_nonce"]) return jsonify({"verified": verified}) # ── Endpoint: POST /appstle/approved (called BY Appstle when merchant approves) ── @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active pass # mark connection as active in your DB r.delete(f"appstle:pending:{shop_domain}") return jsonify({"success": True}) # ── Endpoint: POST /appstle/connect (called BY Appstle during Flow B) ── @app.route("/appstle/connect", methods=["POST"]) def appstle_connect(): body = request.json shop_domain = body["shop_domain"] callback_url = body["callback_url"] callback_nonce = body["callback_nonce"] # Auto-approve: call back immediately # (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try: resp = requests.post( callback_url, json={"shop_domain": shop_domain, "callback_nonce": callback_nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) data = resp.json() if data.get("verified") and data.get("access_token"): save_token(shop_domain, data["access_token"]) except Exception as e: app.logger.error(f"Handshake failed: {e}") return jsonify({"success": True}) ``` ## FAQ Yes. Each partner gets its own scoped API token. Merchants can connect as many partners as they want. The tokens are completely independent. All active partner connections for that shop are automatically disconnected. Your tokens will stop working (401 responses). Yes — contact the Appstle team. New connections will use the updated permission, but existing connections keep their original permission until reconnected. Partner tokens share the same rate limits as regular API keys. If you receive a `429 Too Many Requests`, implement exponential backoff. Use a Shopify development store with Appstle Bundles installed. The partner integration works identically in development and production. You can use a tool like [ngrok](https://ngrok.com) to expose your local endpoints to the internet for testing. The merchant simply needs to click "Connect" again. A new nonce will be generated. Old nonces are automatically cleaned up. Yes. Flow A is for when the merchant connects from your dashboard. Flow B is for when they connect from Appstle's dashboard. Both are needed for a complete integration. You also need the `/appstle/approved` endpoint to receive API tokens after merchant approval (Flow A). No. A new connect handshake requires the merchant to initiate it from one of the dashboards. This is by design — merchants must explicitly authorize each connection. For security and trust. When your app initiates a connection, the merchant hasn't explicitly agreed on Appstle's side. The approval step ensures merchants consciously grant API access to partner apps. Appstle-initiated connections (Flow B) skip this step because the merchant is already clicking "Connect" in the Appstle dashboard. Pending connection requests expire after **30 days**. If the merchant doesn't approve or reject within that window, the request expires and your app will need to initiate a new connection. The status changes to `rejected` and your app is notified via the disconnect webhook (if configured). The merchant can be asked to reconnect later if they change their mind — your app can initiate a new connection request. ## Need help? * **Partner onboarding & technical support:** [support@appstle.com](mailto:support@appstle.com) * **Integration guide:** [Third-party integration guide](/bundles/integration-guide) (for direct API key usage) # Get started with Appstle Bundles API Source: https://developers.appstle.com/bundles/quickstart Create an API key, read bundle rules, and inspect build-a-box storefront endpoints. This guide walks through the fastest path to make your first Appstle Bundles API call. ## Prerequisites * An active Appstle Bundles installation on your Shopify store * Access to the Appstle admin panel * An API key for server-side Admin API calls ## Step 1 — Get your API key In your Appstle admin panel, go to **Settings → API Key Management**. Click **Create New Key** and name it something clear, such as `Bundles quickstart`. Store the key as an environment variable: ```bash theme={null} export APPSTLE_API_KEY="apst_your-api-key-here" export SHOP="your-store.myshopify.com" ``` ## Step 2 — Read bundle rules Call `GET /api/external/bundle-rules` to retrieve configured bundle rules. ```bash curl theme={null} curl -X GET "https://bundles-admin.appstle.com/api/external/bundle-rules?shop=${SHOP}" -H "X-API-Key: ${APPSTLE_API_KEY}" ``` ```javascript Node.js theme={null} const response = await fetch( `https://bundles-admin.appstle.com/api/external/bundle-rules?shop=${process.env.SHOP}`, { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY } } ); const rules = await response.json(); console.log(rules); ``` ```python Python theme={null} import os import httpx response = httpx.get( 'https://bundles-admin.appstle.com/api/external/bundle-rules', params={'shop': os.environ['SHOP']}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) print(response.json()) ``` ## Step 3 — Explore discount and build-a-box rules Use the related Admin API endpoints when you need more specific rule types: | Endpoint | Use it for | | ----------------------------------------- | ------------------------ | | `GET /api/external/bundle-rules` | All bundle rules | | `GET /api/external/bundle-rules/discount` | Bundle discount rules | | `GET /api/external/bundle-rules/bab` | Build-a-box bundle rules | ## Step 4 — Review storefront endpoints For customer-facing build-a-box flows, use the **Storefront API** section in the sidebar. It includes endpoints for fetching build-a-box configuration and generating bundle discounts by token. Storefront API calls depend on Appstle's storefront flow and tokenized build-a-box URLs. Use the generated API reference for exact parameters and response schemas. ## What to build next Review Admin and Storefront endpoint categories for production integrations. Use the Bundles tab sidebar to inspect every endpoint, parameter, and response schema. # Volume discount bundle with a custom storefront Source: https://developers.appstle.com/bundles/volume-discount-bundle Run tiered 'buy more, save more' discounts based on quantity or spend, using your own storefront UI and Appstle's automatic discount. A **volume discount bundle** rewards customers for buying more: the discount grows as they cross quantity or spend thresholds. For example, *"Buy 3, save 10% · Buy 6, save 20%"*. Appstle applies the **best matching tier** automatically. This guide covers running a volume discount bundle from your own storefront UI. You use the *same* metafield, cart-line attributes, and Shopify Function that Appstle's built-in widget uses — only the UI is yours. Read the [headless overview](/bundles/headless-overview) first for the automatic-discount model. | | | | ----------------------- | --------------------------------------------- | | **`bundleType`** | `VOLUME_DISCOUNT` | | **`discountType`** | `TIERED_DISCOUNT` | | **Read from** | `appstle_bundles.bundle_rules` shop metafield | | **Discount applied by** | Appstle automatic discount (Shopify Function) | ## How pricing works The tiers live in the rule's `tieredDiscount` field as a **JSON-encoded string**. Parse it into an array of tier objects: | Tier field | Type | Meaning | | --------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | `discountBasedOn` | string | `QUANTITY` (threshold on item count) or `AMOUNT` (threshold on spend, in the store currency's major unit). | | `value` | integer | The threshold to reach this tier (e.g. `3` items, or `50` for \$50 spend). | | `discount` | number | The discount magnitude. | | `discountType` | string | `PERCENTAGE` or `FIXED_AMOUNT`. | | `titleLabel` / `subtitleLabel` / `badgeLabel` | string | Optional display labels you can reuse in your UI. | **Tier selection** — Appstle evaluates every qualifying tier and applies the one with the **highest discount**: 1. Among `QUANTITY` tiers, keep those where `cartQuantity ≥ value`. 2. Among `AMOUNT` tiers, keep those where `subtotal ≥ value`. 3. From all qualifying tiers, apply the one with the largest `discount`. ## Step 1 — Read the rule and parse the tiers Read the active rules from the **`appstle_bundles.bundle_rules`** shop metafield — the Shopify-native source, with no call to Appstle's servers (see [Reading bundle configuration](/bundles/headless-overview#reading-bundle-configuration)). Filter to volume discount bundles by `bundleType`, then parse the `tieredDiscount` JSON string. ```liquid theme={null} {% assign bundle_rules = shop.metafields.appstle_bundles.bundle_rules.value %} ``` ```javascript theme={null} const rules = window.MY_BUNDLE_RULES || []; const [rule] = rules.filter( r => r.bundleType === 'VOLUME_DISCOUNT' && r.status === 'ACTIVE' ); // tieredDiscount is a JSON string inside the rule — parse it. const tiers = JSON.parse(rule.tieredDiscount || '[]'); // [{ discountBasedOn: "QUANTITY", value: 3, discount: 10, discountType: "PERCENTAGE" }, ...] ``` Fields you need: | Field | Use | | ----------------------- | ---------------------------------------------- | | `uniqueRef` | Attach to every cart line as `_appstle-bb-id`. | | `name` | Display label; attach as `__appstle-bb-name`. | | `tieredDiscount` | JSON string of tier objects (parse it). | | `products` / `variants` | Eligible products and variants (JSON). | ## Step 2 — Render the tier ladder Build your own UI from the parsed tiers: * Show the ladder (e.g. *"Buy 3 → 10% off · Buy 6 → 20% off"*) using `value`, `discount`, and the optional labels. * Track the running quantity and subtotal, highlight the tier the customer currently qualifies for, and nudge them toward the next one. ## Step 3 — Add to cart Add the eligible variants with the [bundle attributes](/bundles/headless-overview#required-cart-line-attributes). For volume discount, `_appstle_bundles_type` is `VOLUME_DISCOUNT`. ```js Shopify theme (Ajax Cart API) theme={null} const line = (variantId, quantity) => ({ id: variantId, quantity, properties: { '_appstle-bb-id': rule.uniqueRef, '_appstle_bundles_type': 'VOLUME_DISCOUNT', '__appstle-bb-name': rule.name, }, }); await fetch(`${Shopify.routes.root}cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: selection.map(s => line(s.variantId, s.quantity)) }), }); ``` ```graphql Headless (Storefront API) theme={null} mutation AddVolumeBundle($cartId: ID!) { cartLinesAdd( cartId: $cartId lines: [ { merchandiseId: "gid://shopify/ProductVariant/123" quantity: 6 attributes: [ { key: "_appstle-bb-id", value: "summer-set-ref" } { key: "_appstle_bundles_type", value: "VOLUME_DISCOUNT" } { key: "__appstle-bb-name", value: "Summer Skincare Set" } ] } ] ) { cart { id } userErrors { field message } } } ``` ### Tier restriction (optional) When the merchant enables **tier restriction**, the discount is pinned to one explicitly chosen tier rather than auto-selecting the best qualifying one. In that mode, also attach the selected tier's identifier: | Attribute | Value | | -------------------------- | ------------------------------------------------- | | `_appstle_bundle_combo_id` | The identifier of the tier the customer selected. | ```js theme={null} properties: { '_appstle-bb-id': rule.uniqueRef, '_appstle_bundles_type': 'VOLUME_DISCOUNT', '__appstle-bb-name': rule.name, '_appstle_bundle_combo_id': selectedTierId, // only when tier restriction is on } ``` Only attach `_appstle_bundle_combo_id` when tier restriction is enabled for the bundle. Without restriction, omit it and the Function applies the best qualifying tier automatically. ## Step 4 — Discount applies automatically Appstle's automatic discount evaluates the qualifying tiers against the cart and applies the best one (or the pinned tier, under restriction) in cart and checkout. As the customer adds or removes quantity, the applied tier updates automatically — no calls from your side. ## Previewing the discount Replicate the best-tier selection for a live preview (cart prices in cents): ```js theme={null} function previewVolumeDiscount(tiers, quantity, subtotalCents) { const subtotalMajor = subtotalCents / 100; const qualifying = tiers.filter(t => (t.discountBasedOn === 'QUANTITY' && quantity >= t.value) || (t.discountBasedOn === 'AMOUNT' && subtotalMajor >= t.value) ); if (qualifying.length === 0) return { discountCents: 0, totalCents: subtotalCents, tier: null }; // Apply the tier with the largest discount. const best = qualifying.reduce((a, b) => (b.discount > a.discount ? b : a)); const discountCents = best.discountType === 'PERCENTAGE' ? Math.round(subtotalCents * (best.discount / 100)) : Math.min(Math.round(best.discount * 100), subtotalCents); return { discountCents, totalCents: subtotalCents - discountCents, tier: best }; } ``` This preview is display-only. The Shopify Function applies the authoritative discount at checkout from the same tiers, so they agree as long as you read `tieredDiscount` from the live rule. ## Checklist ## Next steps Sell a curated set for one fixed price. Build-your-own with a percentage or amount discount. # Enroll customer in loyalty program Source: https://developers.appstle.com/loyalty-admin-api/customer-enrollment-&-profile/enroll-customer-in-loyalty-program /loyalty/admin-api-swagger.json post /api/external/enroll-customer # Get customer loyalty details Source: https://developers.appstle.com/loyalty-admin-api/customer-enrollment-&-profile/get-customer-loyalty-details /loyalty/admin-api-swagger.json get /api/external/customer-loyalty # Update customer birth date Source: https://developers.appstle.com/loyalty-admin-api/customer-enrollment-&-profile/update-customer-birth-date /loyalty/admin-api-swagger.json put /api/external/update-customer-birth-date # Add points to customer account Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/add-points-to-customer-account /loyalty/admin-api-swagger.json post /api/external/add-points # Add store credits to customer account Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/add-store-credits-to-customer-account /loyalty/admin-api-swagger.json post /api/external/add-credits # Approve pending point transactions Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/approve-pending-point-transactions /loyalty/admin-api-swagger.json put /api/external/approve-pending-transactions # Get all point earning rules Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/get-all-point-earning-rules /loyalty/admin-api-swagger.json get /api/external/point-earn-rules # Get customer point transaction history Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/get-customer-point-transaction-history /loyalty/admin-api-swagger.json get /api/external/point-transaction-history/{customer_id} # Get top customers by points earned Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/get-top-customers-by-points-earned /loyalty/admin-api-swagger.json get /api/external/top-customers # Remove points from customer account Source: https://developers.appstle.com/loyalty-admin-api/points-&-earn-rules/remove-points-from-customer-account /loyalty/admin-api-swagger.json post /api/external/remove-points # Check discount code validity Source: https://developers.appstle.com/loyalty-admin-api/redemptions-&-discount-codes/check-discount-code-validity /loyalty/admin-api-swagger.json put /api/external/check-discount # Get all point redemption rules Source: https://developers.appstle.com/loyalty-admin-api/redemptions-&-discount-codes/get-all-point-redemption-rules /loyalty/admin-api-swagger.json get /api/external/point-redeem-rules # Redeem customer points for rewards Source: https://developers.appstle.com/loyalty-admin-api/redemptions-&-discount-codes/redeem-customer-points-for-rewards /loyalty/admin-api-swagger.json post /api/external/redeem-points # Update discount code status Source: https://developers.appstle.com/loyalty-admin-api/redemptions-&-discount-codes/update-discount-code-status /loyalty/admin-api-swagger.json put /api/external/update-discount # Update discount code status (legacy endpoint) Source: https://developers.appstle.com/loyalty-admin-api/redemptions-&-discount-codes/update-discount-code-status-legacy-endpoint /loyalty/admin-api-swagger.json put /api/external/update-discount-code-status # List storefront widget label translations Source: https://developers.appstle.com/loyalty-admin-api/storefront-widgets-&-labels/list-storefront-widget-label-translations /loyalty/admin-api-swagger.json get /api/external/widget-labels # Enable loyalty program for customer Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/enable-loyalty-program-for-customer /loyalty/storefront-api-swagger.json post /loyalty/cp/api/enable-loyalty-program # Enroll customer in loyalty program Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/enroll-customer-in-loyalty-program /loyalty/storefront-api-swagger.json post /loyalty/cp/api/enroll-customer # Get customer loyalty information Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/get-customer-loyalty-information /loyalty/storefront-api-swagger.json get /loyalty/cp/api/customer-loyalty # Get logged-in customer ID Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/get-logged-in-customer-id /loyalty/storefront-api-swagger.json get /loyalty/cp/api/logged-in-customer # Sync customer metafield data Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/sync-customer-metafield-data /loyalty/storefront-api-swagger.json post /loyalty/cp/api/update-customer # Update customer birth date Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/update-customer-birth-date /loyalty/storefront-api-swagger.json post /loyalty/cp/api/update-customer-birth-date # Update customer loyalty status Source: https://developers.appstle.com/loyalty-storefront-api/customer-enrollment-&-profile/update-customer-loyalty-status /loyalty/storefront-api-swagger.json post /loyalty/cp/api/update-customer-status # Claim social media reward points Source: https://developers.appstle.com/loyalty-storefront-api/points-&-earn-rules/claim-social-media-reward-points /loyalty/storefront-api-swagger.json post /loyalty/cp/api/claim-social-media-points # Get customer point transaction history Source: https://developers.appstle.com/loyalty-storefront-api/points-&-earn-rules/get-customer-point-transaction-history /loyalty/storefront-api-swagger.json get /loyalty/cp/api/transaction-by-shop # Track customer store visit Source: https://developers.appstle.com/loyalty-storefront-api/points-&-earn-rules/track-customer-store-visit /loyalty/storefront-api-swagger.json post /loyalty/cp/api/customer-visit-store # Proxy a Shopify Storefront GraphQL request from the customer portal Source: https://developers.appstle.com/loyalty-storefront-api/product-catalog/proxy-a-shopify-storefront-graphql-request-from-the-customer-portal /loyalty/storefront-api-swagger.json post /loyalty/cp/api/storefront-graphql # Redeem customer loyalty points Source: https://developers.appstle.com/loyalty-storefront-api/redemptions-&-discount-codes/redeem-customer-loyalty-points /loyalty/storefront-api-swagger.json post /loyalty/cp/api/redeem-points # Accept referral offer (GET) Source: https://developers.appstle.com/loyalty-storefront-api/referrals/accept-referral-offer-get /loyalty/storefront-api-swagger.json get /loyalty/cp/api/referral-rules/accept-offer # Accept referral offer (POST) Source: https://developers.appstle.com/loyalty-storefront-api/referrals/accept-referral-offer-post /loyalty/storefront-api-swagger.json post /loyalty/cp/api/referral-rules/accept-offer # Generate customer referral URL Source: https://developers.appstle.com/loyalty-storefront-api/referrals/generate-customer-referral-url /loyalty/storefront-api-swagger.json post /loyalty/cp/api/add-customer-referral-url # Get customer referral history Source: https://developers.appstle.com/loyalty-storefront-api/referrals/get-customer-referral-history /loyalty/storefront-api-swagger.json get /loyalty/cp/api/customer-referrals # Send customer referral email Source: https://developers.appstle.com/loyalty-storefront-api/referrals/send-customer-referral-email /loyalty/storefront-api-swagger.json post /loyalty/cp/api/send-customer-referral-url # Get product reviews by product ID Source: https://developers.appstle.com/loyalty-storefront-api/reviews/get-product-reviews-by-product-id /loyalty/storefront-api-swagger.json get /loyalty/cp/api/product-review-details/{productId} # Submit product review Source: https://developers.appstle.com/loyalty-storefront-api/reviews/submit-product-review /loyalty/storefront-api-swagger.json post /loyalty/cp/api/submit-review # Authenticate with the Appstle Loyalty API Source: https://developers.appstle.com/loyalty/authentication Create an API key in the Appstle dashboard, pass it in the X-API-Key header on every request, and manage up to 10 keys per store with per-key revocation. Every Admin API request must carry a valid API key. Keys are created in your Appstle dashboard and scoped to a single Shopify store. There is no OAuth flow — authentication is a single header on every request. ## Creating an API key Log in to your Appstle admin panel and navigate to **Settings → API Key Management**. Click **Create New Key** and enter a descriptive name that identifies the integration — for example, `Klaviyo sync`, `Mobile app`, or `Internal dashboard`. Good names make it easy to audit and revoke keys later. The full key value is shown **only once**. Copy it and store it in your secrets manager or environment variable before leaving the page. If you navigate away without copying it, you must create a new key. Add the key to your application as an environment variable: ```bash theme={null} APPSTLE_API_KEY=apst_your-api-key-here ``` Never hard-code the key in source code or commit it to version control. API keys grant full access to your store's loyalty data. Treat them like passwords. Never expose them in client-side JavaScript, browser extensions, or public repositories. ## Using the API key Include your key in the `X-API-Key` header on every Admin API request. ```bash curl theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=your-store.myshopify.com&customer_id=12345" \ -H "X-API-Key: apst_your-api-key-here" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=your-store.myshopify.com&customer_id=12345', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); const data = await response.json(); ``` ```python Python theme={null} import httpx response = httpx.get( 'https://loyalty-admin.appstle.com/api/external/customer-loyalty', params={'shop': 'your-store.myshopify.com', 'customer_id': '12345'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) data = response.json() ``` You can also pass the key as a query parameter, though the header approach is strongly preferred: ``` ?api_key=apst_your-api-key-here ``` Create one API key per integration. If a single key is compromised or a tool is decommissioned, you can revoke it without disrupting your other integrations. ## Key management You can create up to **10 active API keys** per store. Each key has: * A **display name** you choose at creation time * A **last-used timestamp** so you can identify stale keys * **Individual revocation** — revoking one key does not affect others ### Rotating a key safely Go to **Settings → API Key Management** and create a new key with the same or updated name. Deploy the new key value to your application and verify it works. Return to the dashboard and revoke the old key. Revocation is immediate. ## Partner integrations If you are building an app that connects to multiple merchants' stores, use the [Partner Integration Framework](/loyalty/partner-integration) instead of asking merchants to share API keys manually. The framework: * Provisions a scoped `apst_` token per merchant automatically during a one-click handshake * Lets merchants approve, review, and revoke access from their own dashboard * Bypasses the paid API plan — merchants are never charged for partner API usage * Revokes tokens automatically when a merchant disconnects or uninstalls Appstle Once a merchant approves the connection, send their scoped token as `X-API-Key`, exactly like a regular API key. See the [Partner integration guide](/loyalty/partner-integration) for the complete connection flow. ## Error responses | Status | Cause | Resolution | | ----------------------- | ---------------------------------------------- | ------------------------------------------------------------ | | `401 Unauthorized` | Key is missing, malformed, or revoked | Check the header name and value; create a new key if revoked | | `403 Forbidden` | Key is valid but lacks the required permission | Verify the key's permission level in the dashboard | | `429 Too Many Requests` | Rate limit exceeded | Implement exponential backoff before retrying | # Appstle Loyalty third-party integration guide Source: https://developers.appstle.com/loyalty/integration-guide Build a production integration with Appstle Loyalty. Covers authentication, base URL, point management, enrollment, rewards, store credits, and partner connections. This guide covers everything you need to build a production-ready integration with Appstle Loyalty. It walks through authentication, every major endpoint category, and common integration patterns for the most popular tool types (CRMs, email platforms, review apps). ## Base URL All Admin API endpoints share the same base: ``` https://loyalty-admin.appstle.com ``` Every endpoint is prefixed with `/api/external/`. ## Authentication Direct integrations pass the merchant's API key in the `X-API-Key` header. Merchants create keys under **Settings → API Key Management** in the Appstle admin. Each key is scoped to a single store, and up to 10 active keys are allowed per store. ```bash theme={null} curl -H "X-API-Key: apst_your-api-key-here" \ "https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=your-store.myshopify.com&customer_id=12345" ``` Direct API access requires an active API plan. Contact [support@appstle.com](mailto:support@appstle.com) for details. If you are building a product that connects to multiple merchants (a helpdesk, CRM, review platform, or email tool), use the [Partner Integration Framework](/loyalty/partner-integration). Your app receives a scoped `apst_` API token for each merchant through a one-click handshake — no manual key exchange needed. Partner tokens: * Bypass the paid API plan (merchants are never billed for partner API usage) * Are provisioned automatically during the handshake * Are revoked instantly when a merchant disconnects or uninstalls Appstle Once you have a token, use it exactly like a merchant API key in the `X-API-Key` header. See [Becoming a partner](#becoming-a-partner) for onboarding. ## Point management ### Look up a customer's loyalty data Retrieve a complete loyalty profile by Shopify customer ID or email. ```bash theme={null} # By customer ID curl -X GET \ "https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=your-store.myshopify.com&customer_id=12345" \ -H "X-API-Key: YOUR_API_KEY" ``` The response includes available, pending, and credited points; current VIP tier; store credit balance; referral link; active rewards; and social engagement status. ### Add points Credit points to a customer's account. Include a `note` so the transaction is labeled clearly in the customer's history. ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/external/add-points" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "shop": "your-store.myshopify.com", "customerId": 12345, "points": 100, "note": "VIP welcome bonus" }' ``` ### Redeem points for a reward Convert a customer's points into a discount code using a configured redemption rule. ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/external/redeem-points" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "shop": "your-store.myshopify.com", "customerId": 12345, "pointRedeemRuleId": 42 }' ``` To get the list of valid `pointRedeemRuleId` values, call `GET /api/external/point-redeem-rules` (see [Program configuration](#program-configuration) below). ### Get transaction history Retrieve a customer's full point transaction log. ```bash theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/point-transaction-history/12345?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Approve pending transactions For programs where points require approval before becoming available, use this endpoint to move pending points to available. ```bash theme={null} curl -X PUT \ "https://loyalty-admin.appstle.com/api/external/approve-pending-transactions?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"customerId": 12345}' ``` ## Customer management ### Enroll a customer Add a customer to the loyalty program. Call this before attempting to add points to a customer who has not yet joined. ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/external/enroll-customer" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "shop": "your-store.myshopify.com", "customerId": 12345, "customerEmail": "customer@example.com" }' ``` ### Update a customer's birthday Set a date of birth to enable birthday reward automation. Date format is `YYYY-MM-DD`. ```bash theme={null} curl -X PUT \ "https://loyalty-admin.appstle.com/api/external/update-customer-birth-date?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "customerId": 12345, "birthDate": "1990-06-15" }' ``` ### Get top customers Retrieve your highest-value loyalty members ranked by points. ```bash theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/top-customers?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Rewards and discounts ### Check a discount code Validate a loyalty discount code before applying it at checkout. Returns whether the code is valid, unused, and which customer it belongs to. ```bash theme={null} curl -X PUT \ "https://loyalty-admin.appstle.com/api/external/check-discount?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"discountCode": "REWARD-XXXX"}' ``` ### Mark a discount as used After a customer applies a reward code at checkout, mark it as used to prevent reuse. ```bash theme={null} curl -X PUT \ "https://loyalty-admin.appstle.com/api/external/update-discount-code-status?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "discountCode": "REWARD-XXXX", "status": "USED" }' ``` ## Store credits ### Add store credits Credit a monetary amount to a customer's store credit balance. Credits are applied at checkout and are separate from loyalty points. ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/external/add-credits" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "shop": "your-store.myshopify.com", "customerId": 12345, "credits": 10.00, "note": "Goodwill credit for support issue" }' ``` ## Program configuration ### Get point earn rules Retrieve all active point earn rules for your store. Use the returned `id` values when calling `add-points` with a specific rule. ```bash theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/point-earn-rules?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Get point redeem rules Retrieve all active redemption options. Use the returned `id` values as `pointRedeemRuleId` when redeeming points. ```bash theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/point-redeem-rules?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Rate limits If you receive a `429 Too Many Requests` response, implement exponential backoff before retrying. Do not retry immediately. ## Common integration patterns 1. Customer submits a review on your platform. 2. Your platform calls `POST /api/external/add-points` to credit points. 3. Optionally, use the Shopify Flow `reward-points-for-reviews` action for a no-code setup that does not require API calls. 1. Call `GET /api/external/customer-loyalty` by customer email or ID to display loyalty status alongside the ticket. 2. Call `POST /api/external/add-points` or `POST /api/external/add-credits` to issue goodwill adjustments. 3. Call `GET /api/external/point-transaction-history/{customer_id}` to show recent activity in context. 1. Use webhooks to receive real-time point and tier events for trigger-based campaigns. 2. Call `GET /api/external/customer-loyalty` to enrich customer profiles with points balance, VIP tier, and referral link. 3. Segment customers by `currentVipTier` or `availablePoints` range for targeted sends. ## Webhooks and Shopify Flow For real-time event notifications, see the [Webhooks guide](/loyalty/webhooks). For no-code automation without managing webhook infrastructure, see the [Shopify Flow guide](/loyalty/shopify-flow). ## Becoming a partner If your product integrates with loyalty programs, Appstle offers a formal partner program: * Zero-friction merchant onboarding via the Partner Integration Framework * Scoped API tokens per merchant — no manual key exchange * Merchants are never charged for partner API usage * Technical support, co-marketing opportunities, and directory listing To get onboarded, email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email. The [Partner integration guide](/loyalty/partner-integration) lists the complete onboarding requirements and implementation steps. # Appstle Loyalty: Points, VIP Tiers & Referrals Source: https://developers.appstle.com/loyalty/introduction Appstle Loyalty provides Admin and Storefront REST APIs to build points programs, VIP tiers, and referrals on Shopify. Learn which API fits your use case. Appstle Loyalty is a complete loyalty and rewards platform for Shopify stores. Once you install the app, Appstle exposes two REST API surfaces so you can build anything from simple points programs to multi-tier VIP schemes with referrals, reviews, and store credits on top of your loyalty data. ## Available APIs Appstle Loyalty exposes two REST API surfaces. Pick the one that matches where your code runs: * **Admin API** — server-side, authenticated with `X-API-Key`. For backend integrations, mobile apps, automation, and admin dashboards. Browse the full reference under **Admin API** in the sidebar. * **Storefront API** — customer-facing, accessed through Shopify App Proxy. For loyalty widgets and self-service portals on your storefront. Browse the full reference under **Storefront API** in the sidebar. ## Which API should you use? * You are building a **server-side integration** (backend service, CRM connector, helpdesk plugin) * You are building a **mobile app** (iOS or Android) * You need to **credit or debit points programmatically** on behalf of merchants * You want to run **bulk operations** or scheduled automation * You are building an **admin dashboard** or reporting tool * Your code runs **anywhere outside a customer's browser** on the storefront Admin API requests require an `X-API-Key` header. Keys are created in the Appstle dashboard under **Settings → API Key Management**. Never expose keys in client-side code. * You are building a **custom loyalty portal** hosted on your Shopify storefront * You want customers to **earn and redeem points** themselves (widgets, referral pages, review forms) * Your code runs **inside the storefront** with a logged-in customer session * You are customizing the loyalty widget **theme or appearance** Storefront APIs run exclusively through Shopify's App Proxy and require the customer to be logged in. They will not accept API key authentication and cannot be called from a backend server. ## Key features Credit and debit points for purchases, reviews, social actions, birthdays, and custom activities. Access full transaction history. Define spend or points thresholds for Bronze, Silver, Gold, Platinum, or any tier structure. Automatically promote and demote customers. Generate unique referral links, track accepted referrals, and reward both the referrer and the referred customer. Let customers submit reviews and earn points. Display ratings via Shopify metafields compatible with native rich results. Issue monetary store credit as an alternative to discount codes. Balances are tracked separately from loyalty points. Convert points into Shopify discount codes. Validate and mark codes as used from your own checkout flow. ## Key concepts Points move through three states: **pending** (earned but awaiting approval), **available** (redeemable), and **credited** (total lifetime points earned). Your program configuration determines whether points are credited immediately or held in a pending state for review. Tiers are assigned automatically when a customer crosses a configured spend or points threshold. Each tier can carry a name tag and any number of additional Shopify customer tags. Tags are swapped automatically on tier changes — there is always at most one active tier per customer. Every enrolled customer receives a unique referral link. When a referred customer makes a qualifying purchase, both parties receive rewards as defined in your referral configuration. Referral relationships are recorded permanently as Shopify customer tags. Store credits are a monetary balance separate from points. They are applied at checkout and tracked in `storeCreditBalance` on the customer loyalty profile. Credits can be issued via the Admin API or through Shopify Flow. Third-party apps (helpdesks, CRMs, review platforms, email tools) can connect to Appstle Loyalty through the Partner Integration Framework. Partners receive a scoped API token per merchant with no manual key exchange. Merchants connect and disconnect with one click. ## Base URL All Admin API endpoints use: ``` https://loyalty-admin.appstle.com ``` Storefront API endpoints are accessed through your store's Shopify App Proxy — no separate base URL is needed. ## HTTP status codes All API responses use standard HTTP status codes. | Code | Meaning | | ----- | ------------------------------------------ | | `200` | Success | | `201` | Resource created | | `400` | Bad request — invalid parameters | | `401` | Unauthorized — missing or invalid API key | | `403` | Forbidden — key lacks required permissions | | `404` | Not found | | `429` | Rate limit exceeded | | `500` | Server error | Error responses follow this shape: ```json theme={null} { "error": "Unauthorized", "message": "Invalid API key provided", "status": 401 } ``` ## Next steps Create API keys and learn how to authenticate every request. Get your API key, make your first request, and add points in under five minutes. Full walkthrough of every endpoint category with working curl examples. Receive real-time notifications for points, tier changes, and referrals. # Shopify metafields and tags for Appstle Loyalty Source: https://developers.appstle.com/loyalty/metafields-and-tags Shopify metafields and tags set by Appstle Loyalty: loyalty profiles, reward codes, product reviews, VIP tags, and referral tags — with schema examples. Appstle Loyalty uses Shopify metafields and customer tags to persist loyalty data, power storefront widgets, surface product reviews, and enable automation in Shopify Flow and Liquid themes. This page documents every metafield and tag — what it contains, when it is updated, and how to use it in your integration or theme. ## Metafield namespaces Appstle Loyalty writes to three namespaces across shop, customer, and product resources. | Namespace | Resources | Visibility | Purpose | | ---------------------- | -------------- | ---------- | --------------------------------------------------- | | `appstle_loyalty` | Shop, Customer | Public | Loyalty configuration and customer loyalty profiles | | `$app:appstle_loyalty` | Customer | Private | Customer reward discount codes for checkout | | `appstle_review` | Product, Shop | Public | Product reviews, ratings, and carousel data | The `$app:appstle_loyalty` namespace uses Shopify's app-owned metafield protection. Only the Appstle Loyalty app can read and write it. Themes, Liquid templates, and other apps cannot access it. ## Shop metafields — `appstle_loyalty` These metafields store the complete loyalty program configuration. They are written whenever a merchant saves settings in the Appstle admin, and updates are immediate. ### Core configuration | Key | Type | Description | | -------------------------- | ------ | -------------------------------------------------- | | `app_url` | string | App base URL for widget communication | | `proxy_path_prefix` | string | Custom proxy path prefix (default: `apps/loyalty`) | | `public_domain` | string | Store's public domain | | `shop_name` | string | Store name from Shopify | | `currency` | string | ISO currency code (e.g., `USD`, `EUR`) | | `store_front_access_token` | string | Shopify Storefront API access token | ### Widget configuration | Key | Type | Description | | ----------------- | ---- | ------------------------------------------------------- | | `bundle_js_path` | url | CDN URL for the widget JavaScript bundle | | `bundle_css_path` | url | CDN URL for the widget CSS bundle | | `widget_setting` | json | Full widget configuration (colors, layout, positioning) | | `shop_labels` | json | Widget label translations for i18n support | ### Points configuration | Key | Type | Description | | -------------------- | ------ | ---------------------------------------------------------- | | `pointRoundType` | string | Rounding strategy: `NO_ROUND`, `ROUND_DOWN`, or `ROUND_UP` | | `point_earn_rules` | json | Array of active point earn rules | | `point_redeem_rules` | json | Array of active point redemption rules | ```json theme={null} [ { "id": 123, "type": "PURCHASE", "pointsPerDollar": 5, "minimumOrderAmount": 0, "enabled": true }, { "id": 124, "type": "SIGNUP", "fixedPoints": 100, "enabled": true }, { "id": 125, "type": "BIRTHDAY", "fixedPoints": 200, "enabled": true } ] ``` ```json theme={null} [ { "id": 456, "type": "FIXED_AMOUNT", "pointsCost": 500, "discountValue": 5.00, "discountType": "FIXED_AMOUNT", "enabled": true }, { "id": 457, "type": "PERCENTAGE", "pointsCost": 1000, "discountValue": 10, "discountType": "PERCENTAGE", "maxDiscountAmount": 50.00, "enabled": true } ] ``` ### Feature flags | Key | Type | Description | | ---------------------------------------- | ------- | -------------------------------------------------- | | `enable_inactive_customer` | boolean | Allow tracking inactive customers | | `has_dedicated_page_access` | boolean | Dedicated loyalty page feature enabled | | `allow_customer_opt_in` | boolean | Customers can self-enroll in the program | | `enable_discount_to_apply_automatically` | boolean | Auto-apply reward discounts at checkout | | `show_store_credit_rewards` | boolean | Show store credit rewards in the widget | | `birthdate_format` | string | Expected birthdate format (`MM/DD`, `DD/MM`, etc.) | ### VIP tier configuration | Key | Type | Description | | ------------------- | ------- | ---------------------------------- | | `vip_tier_enabled` | boolean | VIP tier program enabled | | `vip_tiers` | json | Array of VIP tier definitions | | `vip_rewards` | json | VIP tier-specific redemption rules | | `vip_point_rewards` | json | VIP tier-specific earn rules | | `vip_tier_setting` | json | VIP tier global configuration | ```json theme={null} [ { "id": 1, "name": "Silver", "threshold": 0, "icon": "🥈", "additionalTags": "silver-member" }, { "id": 2, "name": "Gold", "threshold": 1000, "icon": "🥇", "additionalTags": "gold-member, premium-support" }, { "id": 3, "name": "Platinum", "threshold": 5000, "icon": "💎", "additionalTags": "platinum-member, priority-shipping, premium-support" } ] ``` ### Referral configuration | Key | Type | Description | | ------------------ | ------- | --------------------------------------------------- | | `referral_enabled` | boolean | Referral program enabled | | `referral_loyalty` | json | Referral discount types, amounts, and point rewards | ```json theme={null} { "referrerDiscountType": "PERCENTAGE", "referrerDiscountValue": 10, "referredDiscountType": "FIXED_AMOUNT", "referredDiscountValue": 5.00, "referrerPoints": 500, "referredPoints": 200 } ``` ### Points expiration | Key | Type | Description | | --------------------------- | ---- | ---------------------------- | | `points_expiration_setting` | json | Expiration interval and type | ```json theme={null} { "enabled": true, "interval": 12, "intervalType": "MONTH" } ``` ## Customer metafields — `appstle_loyalty` ### `customer_loyalty` The customer loyalty profile is stored as a JSON metafield on each enrolled customer. It is updated by the Appstle Lambda processor after every loyalty event — points earned or redeemed, VIP tier changes, referrals, social engagement, and more. Updates are near real-time, typically within a few seconds. **Access in Liquid:** ```liquid theme={null} {{ customer.metafields.appstle_loyalty.customer_loyalty }} ``` **Full schema:** ```json theme={null} { "availablePoints": 1250, "pendingPoints": 100, "redeemedPoints": 500, "creditedPoints": 1850, "vipTier": { "id": 2, "name": "Gold" }, "achievableTierId": 3, "activeRewards": [ { "discountCode": "REWARD-XYZ789", "discountType": "PERCENTAGE", "discountValue": 10 } ], "socialMediaEngagement": { "facebook": true, "twitter": false, "instagram": true, "youtube": false, "tiktok": false, "pinterest": false, "newsletter": true, "sms": false, "accountCreation": true, "sharing": false }, "referralInfo": { "referralLink": "https://store.myshopify.com/?ref=abc123", "completedReferrals": 3 }, "customerStatus": "ACTIVE", "dateOfBirth": "1990-05-15", "storeCreditBalance": 25.00, "lastActivityDate": "2025-03-20T14:30:00Z", "spentAmount": 450.75 } ``` ## Customer metafields — `$app:appstle_loyalty` ### `customer_rewards` Stores the customer's unredeemed reward discount codes. This metafield is private — it is used by Shopify's discount function to auto-apply rewards at checkout and cannot be read by themes, Liquid templates, or other apps. ```json theme={null} { "rewards": [ { "discountCode": "REWARD-ABC123" }, { "discountCode": "REWARD-DEF456" } ] } ``` ## Product metafields — `appstle_review` Review metafields are written on individual product resources when a review is approved, updated, or deleted. Access them in Liquid via `{{ product.metafields.appstle_review. }}`. | Key | Type | Description | | ------------------------ | ------- | ------------------------------------------------------------- | | `product_reviews` | json | Approved reviews for the product page (max 5 per page) | | `total_reviews` | integer | Total review count | | `rating` | rating | Overall rating using Shopify's native rating type (scale 1–5) | | `product_rating_details` | json | Breakdown by star rating | ```json theme={null} [ { "id": 789, "author": "John D.", "rating": 5, "title": "Amazing product!", "body": "This is the best coffee I've ever had.", "createdAt": "2025-03-15T10:00:00Z", "verified": true } ] ``` ```json theme={null} { "value": "4.5", "scale_min": "1", "scale_max": "5" } ``` The native `rating` type integrates directly with Shopify's built-in rating display and Google rich results. ```json theme={null} { "5": 42, "4": 18, "3": 7, "2": 2, "1": 1 } ``` ## Shop metafields — `appstle_review` These metafields power the review carousel widget on your homepage. | Key | Type | Description | | -------------------------- | ------- | --------------------------------------- | | `carousel_product_reviews` | json | Recent or featured reviews (max 15) | | `carousel_total_reviews` | integer | Total review count for the carousel | | `carousel_rating` | rating | Overall rating for the carousel display | ## Customer tags Appstle Loyalty applies tags only to customer resources — not orders or products. ### VIP tier name tags When a customer achieves or is assigned a VIP tier, the tier's name is applied as a customer tag (e.g., `Silver`, `Gold`, `Platinum`). When the tier changes, the old name tag is removed and the new one is added. Only one tier name tag is active at a time. **Tags are applied by:** * Automatic tier calculation based on spend, points, or order thresholds * Manual tier assignment in the Appstle admin * Referral flow tier assignment * Shopify Flow `Assign VIP Tier` action * Bulk operations ### Additional VIP tier tags Each VIP tier can be configured with a comma-separated list of `additionalTags`. These are applied alongside the tier name tag and removed together with it on tier change. For example, a Gold tier configured with `additionalTags = "premium-member, vip-support"` applies three tags when a customer reaches Gold: `Gold`, `premium-member`, `vip-support`. All three are replaced when the customer moves to the next tier. ### Referral tags When a referral is accepted by both parties, permanent tags are applied to record the relationship. | Tag pattern | Applied to | Description | | ------------------------------- | --------------------- | ----------------------------------- | | `referral:{referredCustomerId}` | The referrer | Records which customer was referred | | `referred:{referrerCustomerId}` | The referred customer | Records who referred them | These tags are never removed. **Example:** Customer A (ID `1111`) refers Customer B (ID `2222`): * Customer A receives tag `referral:2222` * Customer B receives tag `referred:1111` ### Product review tag The tag `Wrote Appstle Web Review` is permanently applied to any new Shopify customer account that is created as a result of a product review submission (i.e., the customer did not already have a Shopify account). ## Frequently asked questions Yes. Metafields in the `appstle_loyalty` and `appstle_review` namespaces are public. Access them in Liquid via `{{ customer.metafields.appstle_loyalty.customer_loyalty }}` or `{{ product.metafields.appstle_review.rating }}`. The `customer_rewards` metafield in the `$app:appstle_loyalty` namespace is private and cannot be accessed from Liquid or other apps. Customer loyalty metafields are updated by the Lambda processor after every loyalty event. Updates are near real-time — typically within a few seconds of the event occurring. When a customer's tier changes, the old tier name tag and all its configured `additionalTags` are removed. The new tier name tag and its `additionalTags` are then added. Only one tier's tags are active at any given time. No. The `customer_rewards` metafield uses the `$app:appstle_loyalty` private namespace, which can only be read and written by the Appstle Loyalty app. This prevents discount codes from being exposed to unauthorized apps or themes. # Partner Integration Framework overview Source: https://developers.appstle.com/loyalty/partner-framework-overview How the Appstle Loyalty Partner Integration Framework works — one handshake per merchant, a scoped API token, no API plan required, automatic revocation. The Partner Integration Framework lets your product — a helpdesk, CRM, review platform, or email tool — connect to Appstle Loyalty on behalf of many merchants. Instead of asking each merchant to create and paste an API key, your app completes a one-time handshake per store and receives a **scoped API token** for it. ## How a connection works You receive a **Partner ID** and **Partner Secret** used only for connection calls. Either from your product's UI, or from **Settings → Partner Connections** in their Appstle dashboard. Connections your app initiates stay pending until the merchant approves them in Appstle. Pending requests expire after 30 days. Appstle delivers a merchant-specific `apst_...` token to your callback. Send it as `X-API-Key` on Admin API calls — exactly like a regular API key. When a merchant disconnects your app — or uninstalls Appstle — the token is revoked immediately. ## Why use it * **No API plan required** — merchants are never billed for partner API usage * **One isolated token per merchant** — no shared credentials, no manual key exchange, individually revocable * **Merchant-controlled** — merchants see, approve, and disconnect partners from their own dashboard * **Automatic cleanup** — access is revoked the moment a merchant disconnects or uninstalls ## Access levels Your app's permission level is set during onboarding: | Permission | What your app can do | | ---------------- | -------------------------------------------------------------------------------- | | **Read Only** | View customer points, VIP tiers, transaction history, rewards, and referral data | | **Read & Write** | Everything above, plus add or remove points, issue rewards, and enroll customers | ## Connection modes | Mode | Use it when | Your app receives | | ----------------------------- | ------------------------------------------ | --------------------------------------------------- | | **Nonce Handshake** (default) | Your app needs to call Appstle's Admin API | A merchant-scoped `apst_...` API token | | **Simple Token Exchange** | Appstle should push data to *your* API | No Appstle token — Appstle stores a token you issue | ## Get started Email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email to get onboarded. Then follow the [Partner integration guide](/loyalty/partner-integration) for the full implementation — endpoints, handshake, callbacks, and testing. # Partner Integration Framework Source: https://developers.appstle.com/loyalty/partner-integration Build a seamless, zero-configuration integration between your app and Appstle Loyalty. Scoped API tokens, nonce handshake, and merchant-approved connections. Build a seamless, zero-configuration integration between your app and Appstle Loyalty. Once connected, your app gets a scoped API token for each merchant — no manual key exchange needed. **Why become a partner?** * Frictionless merchant onboarding — one-click connect from either dashboard * No API paywall — merchants don't need a paid API plan to use your integration * Scoped tokens — each merchant gets an isolated API key; revocable at any time * Automatic cleanup — when a merchant disconnects or uninstalls, access is revoked instantly ## How it works The Partner Integration Framework uses a secure handshake protocol. Either side — your app or Appstle — can initiate the connection. Both flows end with your app receiving a scoped API token. ```mermaid theme={null} sequenceDiagram autonumber participant P as Partner App participant A as Appstle Loyalty rect rgb(240, 248, 255) Note over P,A: Flow A — Partner initiates P->>A: POST /api/partner/{id}/connect
(shop_domain, callback_nonce, secret) A->>P: POST {your_base_url}/appstle/verify
(nonce check) P-->>A: { "verified": true } A-->>P: { "status": "pending_merchant_approval" } Note over P,A: Merchant approves in Appstle dashboard A->>P: POST {your_base_url}/appstle/approved
(access_token) end rect rgb(245, 245, 250) Note over P,A: Flow B — Appstle initiates A->>P: POST {your_base_url}/appstle/connect
(shop_domain, app, callback_url, nonce) P->>A: POST /api/partner/{id}/verify
(shop_domain, callback_nonce, secret) A-->>P: { "access_token": "..." } end ``` **Merchant approval:** When your app initiates a connection (Flow A), the merchant must approve it from their Appstle dashboard before you receive an API token. When the merchant initiates from Appstle's side (Flow B), the connection is approved instantly because the merchant is the one clicking "Connect." ## Getting started ### Step 1: Get onboarded To get started, reach out to the Appstle team at [support@appstle.com](mailto:support@appstle.com) with the information below. Our team will set up your partner account and send you your credentials. #### What you'll need to provide | # | Field | Required? | Description | Example | | - | ------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | 1 | **App / Company Name** | Yes | Your app or company name. Displayed to merchants when they browse available partner integrations in the Appstle dashboard. | `SearchPie` | | 2 | **Partner ID** | Yes | A unique, lowercase slug that identifies your app in API URLs. Use only lowercase letters and hyphens. Once set, this cannot be changed. | `search-pie` | | 3 | **Base URL** | Yes | The HTTPS base URL where Appstle will send callback requests (connect, verify, approved). Must be publicly accessible — Appstle will not call HTTP or localhost URLs. | `https://api.searchpie.com` | | 4 | **Contact Email** | Yes | The email address where we'll send your Partner Secret and any onboarding follow-ups. Use a team email if possible — the secret is shown only once. | `dev-team@searchpie.com` | | 5 | **Authentication Mode** | Optional | How your API calls are authenticated. Choose one: **Partner Secret** (simpler — pass secret in a header) or **HMAC-SHA256** (more secure — sign each request). Defaults to Partner Secret if not specified. See [Step 1b](#step-1b-choose-your-authentication-mode) for details. | `Partner Secret` | | 6 | **Connect Mode** | Optional | How merchant connections are established. Choose one: **Nonce Handshake** (full two-way verification — you receive an Appstle API key) or **Simple Token Exchange** (streamlined — you provide your own token for Appstle to call your API). Defaults to Nonce Handshake if not specified. See [Step 1c](#step-1c-choose-your-connect-mode) for details. | `Nonce Handshake` | | 7 | **Custom Endpoint Paths** | Optional | By default, Appstle calls `/appstle/connect` and `/appstle/verify` on your Base URL. If you need different paths (e.g., `/webhooks/appstle/connect`), specify them here. | `/webhooks/appstle/connect` | | 8 | **Sync Path** | Optional | If you want Appstle to push loyalty data to your app (e.g., when points are earned, redeemed, or tiers change), provide the path on your server where Appstle should send these payloads. See [Data Sync](#data-sync-push-model) for details. | `/appstle/sync` | | 9 | **App Logo** | Optional | A square logo (PNG or SVG, at least 128×128px) displayed next to your app name in the merchant's Appstle dashboard. If not provided, a placeholder icon is used. | — | Not sure about some of these? Only the first four fields are required to get started. You can always reach out to [support@appstle.com](mailto:support@appstle.com) to change your authentication mode, connect mode, or add a sync path later. **Recommended default for new partners: Simple Token Exchange + Partner Secret.** This is the lowest-friction setup — your app generates a single access token per merchant, hands it to Appstle, and authenticates calls with an `X-Partner-Secret` header. No nonce storage, no HMAC computation, no `/appstle/verify` endpoint to implement. Pick this unless you specifically need Appstle to call your API on behalf of a merchant (use Nonce Handshake) or your security review mandates request signing (use HMAC-SHA256). #### API namespaces — what you call vs. what is internal Three URL namespaces appear in this codebase. **As a third-party partner, you only ever call the first one.** The others exist for Appstle's merchant portal and inter-app integrations and are documented here so the surface area is unambiguous: | Namespace | Who calls it | Purpose | | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/partner/...` | **You** (the partner) | Connect, verify, disconnect, status. Authenticated with your Partner Secret or HMAC. | | `/api/integrations/partner/...` | Appstle merchant-portal UI | Drives the merchant-facing "Connect / Disconnect" buttons in the Appstle dashboard. Session-authenticated; not part of the public partner API. | | `/api/integrations/callback/...` | Other Appstle apps | Receiver-side callbacks for app-to-app integrations between Appstle products. Not used by third-party partners. | If you see an example referring to `/api/integrations/...`, it's an internal Appstle flow and doesn't apply to your integration. #### What you'll receive Once onboarded, you'll receive three values: | Credential | Example | Description | | ------------------ | ----------------------------------- | ----------------------------------------------------------------------------- | | **Partner ID** | `search-pie` | Your unique identifier, as requested. Becomes part of the API URL. | | **Partner Secret** | `xK9mQ2vL...` (48 chars) | A secret key used to authenticate your API calls. Treat this like a password. | | **Base URL** | `https://loyalty-admin.appstle.com` | Appstle's API base URL. Same for all partners. | Your Partner Secret is shown only once during onboarding. Copy it immediately and store it in a secure location (environment variable, secrets manager, etc.). If you lose it, contact Appstle to rotate it — the old secret will be invalidated immediately. **Store your credentials as environment variables:** ```bash theme={null} # .env (never commit this file) APPSTLE_PARTNER_ID=search-pie APPSTLE_PARTNER_SECRET=xK9mQ2vLa8nR3pY... # if using Partner Secret auth APPSTLE_HMAC_KEY=your-hmac-key-here # if using HMAC-SHA256 auth APPSTLE_BASE_URL=https://loyalty-admin.appstle.com ``` ### Step 1b: Choose your authentication mode Appstle supports two ways to authenticate partner API calls. Your auth mode is configured during onboarding. #### Option A: Partner Secret (default) The simplest approach. Pass your secret in a header with every request: ```http theme={null} X-Partner-Secret: your-partner-secret ``` That's it. No computation needed. Good for getting started quickly. #### Option B: HMAC-SHA256 A more secure approach where requests are signed with a shared HMAC key. Instead of sending the secret directly, you compute a signature over the request body. **Headers required:** ```http theme={null} X-Partner-Timestamp: 1709856000 X-Partner-Signature: 5a3c1f2e9b8d7a6c... ``` **How to compute the signature:** 1. Get the current Unix timestamp (seconds, not milliseconds) 2. Concatenate the timestamp and the raw JSON request body: `timestamp + body` 3. Compute HMAC-SHA256 of that string using your HMAC key 4. Send the hex-encoded result in `X-Partner-Signature` **Timestamp validation:** Appstle rejects requests where the timestamp is more than 5 minutes from the server's current time. Make sure your server clock is synced (NTP). ```javascript Node.js theme={null} const crypto = require('crypto'); function signRequest(body, hmacKey) { const timestamp = Math.floor(Date.now() / 1000).toString(); const data = timestamp + body; const signature = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', }; } // Usage const body = JSON.stringify({ shop_domain: 'cool-store.myshopify.com' }); const headers = signRequest(body, process.env.APPSTLE_HMAC_KEY); ``` ```python Python theme={null} import hmac import hashlib import time import json def sign_request(body: str, hmac_key: str) -> dict: timestamp = str(int(time.time())) data = timestamp + body signature = hmac.new( hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256, ).hexdigest() return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', } # Usage body = json.dumps({"shop_domain": "cool-store.myshopify.com"}) headers = sign_request(body, os.environ["APPSTLE_HMAC_KEY"]) ``` ```bash curl theme={null} # Compute signature: HMAC-SHA256(timestamp + body, key) TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://loyalty-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Which should I choose?** * **Partner Secret** — simpler to implement, fine for most integrations * **HMAC-SHA256** — better security (secret never sent over the wire), recommended for high-volume or security-sensitive integrations Both are equally supported. You can switch modes later by contacting Appstle. ### Step 1c: Choose your connect mode Appstle supports two ways to establish merchant connections. Your connect mode is configured during onboarding. #### Option A: Nonce Handshake (default) The full two-way verification flow described in this guide. Both sides verify each other using a one-time nonce. After the handshake, your app receives an Appstle API key (`apst_...`) to call Appstle's External API. **Best for:** partners who want to read/write data in Appstle (customer points, transactions, rewards, etc.) #### Option B: Simple Token Exchange A streamlined flow where your app sends its own access token to Appstle (or Appstle calls your connect endpoint and you return one). No nonce, no verify endpoint needed. Appstle stores your token and uses it to call your API when needed. **Best for:** partners where Appstle needs to call the partner's API (e.g., syncing data to the partner's platform), rather than the partner calling Appstle's API. **Key difference from Nonce Handshake:** in Simple Token Exchange, your app provides its own access token to Appstle. Appstle stores this token and uses it to push data to your API (via your `sync_path` — see [Data Sync](#data-sync-push-model) below). Your app does not receive an Appstle API key in this mode. **Need both directions?** If you need to both push data to Appstle AND have Appstle push data to you, use the Nonce Handshake mode and provide a `sync_path` during onboarding. Contact [support@appstle.com](mailto:support@appstle.com) to discuss your use case. **How Simple Token Exchange works.** *Partner-initiated:* ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: 1709856000" \ -H "X-Partner-Signature: 5a3c1f2e..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "access_token": "your-apps-token-for-this-merchant" }' ``` Response: ```json theme={null} { "status": "pending_merchant_approval" } ``` Your `access_token` is stored securely but will not be activated until the merchant approves the connection from their Appstle dashboard. Once approved, Appstle calls your `/appstle/approved` endpoint to confirm (see [Handling the approval callback](#handling-the-approval-callback)). *Appstle-initiated:* Appstle calls your `/appstle/connect` endpoint with `{ "shop_domain": "..." }`. Your app responds with: ```json theme={null} { "success": true, "access_token": "your-apps-token-for-this-merchant" } ``` With Simple Token Exchange, your app does NOT receive an Appstle API key. If you also need to call Appstle's External API, use the Nonce Handshake mode instead. ### Step 2: Understand the callback nonce This section applies to Nonce Handshake mode only. If you're using Simple Token Exchange, skip to [Step 4](#step-4-implement-the-connect-flow-your-dashboard). Before implementing, you need to understand the callback nonce — it's the core security mechanism of the handshake. #### What is a callback nonce? A nonce (number used once) is a random, single-use string that proves both sides of the connection are who they claim to be. It prevents replay attacks and ensures the handshake can't be forged. #### Requirements | Requirement | Detail | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Length** | At least 32 bytes (64 hex characters) | | **Randomness** | Must be cryptographically random — do NOT use `Math.random()`, `rand()`, timestamps, or UUIDs | | **Single-use** | Each nonce must be used exactly once, then deleted | | **Expiry** | Nonces expire after 5 minutes on Appstle's side. Your storage should also expire them. | | **Storage** | Store temporarily with a TTL. Redis, DynamoDB, or any key-value store with expiry works. Database with a cleanup job is also fine. | #### How to generate a nonce Use your language's cryptographically secure random number generator. ```javascript Node.js theme={null} const crypto = require('crypto'); // Generate a 32-byte (64 hex character) cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```python Python theme={null} import secrets # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = secrets.token_hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```ruby Ruby theme={null} require 'securerandom' # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = SecureRandom.hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```php PHP theme={null} // Generate a 32-byte (64 hex character) cryptographically random nonce $nonce = bin2hex(random_bytes(32)); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```java Java theme={null} import java.security.SecureRandom; SecureRandom secureRandom = new SecureRandom(); byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); StringBuilder sb = new StringBuilder(64); for (byte b : bytes) { sb.append(String.format("%02x", b)); } String nonce = sb.toString(); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```go Go theme={null} import ( "crypto/rand" "encoding/hex" ) bytes := make([]byte, 32) rand.Read(bytes) nonce := hex.EncodeToString(bytes) // Result: "a1b2c3d4e5f6...64 characters total" ``` **Common mistakes:** * `Math.random().toString(36)` — not cryptographically random, predictable * `uuid.v4()` — UUIDs are not designed as security tokens (some implementations use weak RNG) * `Date.now().toString()` — trivially guessable * Reusing nonces across multiple connect attempts Always use your language's `crypto` / `secrets` / `SecureRandom` module. #### How to store a nonce Store the nonce temporarily, keyed by shop domain, with a 5-minute expiry. Delete it after verification. ```javascript Node.js + Redis theme={null} const Redis = require('ioredis'); const redis = new Redis(); // Store nonce with 5-minute TTL async function storeNonce(shopDomain, nonce) { const key = `appstle:nonce:${shopDomain}`; await redis.set(key, nonce, 'EX', 300); // 300 seconds = 5 minutes } // Retrieve and delete nonce (single atomic operation) async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const storedNonce = await redis.get(key); if (!storedNonce || storedNonce !== nonceToCheck) { return false; } await redis.del(key); return true; } ``` ```python Python + database theme={null} from datetime import datetime, timedelta from your_app.models import PartnerNonce # your ORM model def store_nonce(shop_domain: str, nonce: str): # Delete any existing nonce for this shop (prevent duplicates) PartnerNonce.objects.filter(shop_domain=shop_domain).delete() PartnerNonce.objects.create( shop_domain=shop_domain, nonce=nonce, expires_at=datetime.utcnow() + timedelta(minutes=5), ) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: try: record = PartnerNonce.objects.get( shop_domain=shop_domain, nonce=nonce_to_check, expires_at__gt=datetime.utcnow(), # not expired ) record.delete() return True except PartnerNonce.DoesNotExist: return False ``` ### Step 3: Implement your endpoints Your app must expose two HTTP endpoints that Appstle calls during the connection handshake. The paths default to `/appstle/connect` and `/appstle/verify` but can be customized during onboarding. **Both endpoints must:** * Accept `POST` requests with a JSON body * Return JSON responses * Be accessible over HTTPS (Appstle will not call HTTP endpoints) * Respond within 10 seconds (or the request will time out) * **Be idempotent.** Appstle may retry a callback on transient failure, and a merchant flipping connect/disconnect repeatedly will exercise the same endpoint with the same `(shop_domain, partnerId)` pair. Treat every call as an upsert keyed by `(shop_domain, partnerId)` — never blindly insert. The same rule applies to your `/appstle/approved` and `/appstle/disconnect` endpoints described later. #### Endpoint 1: `POST /appstle/connect` **When is this called?** Appstle calls this when a merchant initiates the connection from Appstle's dashboard (Flow B). **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "app": "loyalty", "callback_url": "https://loyalty-admin.appstle.com/api/partner/your-partner-id/verify", "callback_nonce": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" } ``` | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `shop_domain` | string | The merchant's Shopify domain (e.g. `cool-store.myshopify.com`) | | `app` | string | Always `"loyalty"` — identifies which Appstle app is connecting | | `callback_url` | string | The exact URL your app must call to complete the handshake. **The `{partnerId}` embedded in this URL is *Appstle's* identifier for this Appstle app on your side, not your Partner ID.** Use the URL verbatim — don't parse or substitute the segment. | | `callback_nonce` | string | A one-time-use token generated by Appstle. Expires in 5 minutes. | **What should your app do?** 1. **Validate the shop** — check that this `shop_domain` exists in your system. If you don't recognize the shop, return an error. 2. **Store the nonce and callback URL** — save `callback_nonce` and `callback_url` associated with this `shop_domain`. You'll need them to complete the handshake. 3. **Call back to Appstle** — either immediately (auto-approve) or after merchant confirmation, call the `callback_url` to complete the connection. See [Completing the handshake](#completing-the-handshake-flow-b) below. 4. **Return a success response** — any `2xx` status code tells Appstle the request was received. ```javascript Node.js (Express) theme={null} const express = require('express'); const axios = require('axios'); const router = express.Router(); router.post('/appstle/connect', async (req, res) => { const { shop_domain, app, callback_url, callback_nonce } = req.body; // 1. Validate: does this shop exist in your system? const shop = await db.shops.findOne({ domain: shop_domain }); if (!shop) { return res.status(400).json({ error: 'Shop not found in our system' }); } // 2. Store the nonce and callback URL for this shop await db.pendingConnections.upsert({ shopDomain: shop_domain, callbackUrl: callback_url, callbackNonce: callback_nonce, createdAt: new Date(), expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes }); // 3. Option A: Auto-approve (call back immediately) try { const response = await axios.post(callback_url, { shop_domain: shop_domain, callback_nonce: callback_nonce, }, { headers: { 'X-Partner-Secret': process.env.APPSTLE_PARTNER_SECRET, 'Content-Type': 'application/json', }, }); if (response.data.verified && response.data.access_token) { // 4. Store the access token for this merchant await db.appstleTokens.upsert({ shopDomain: shop_domain, accessToken: response.data.access_token, connectedAt: new Date(), }); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } // 5. Return success to Appstle res.json({ success: true }); }); ``` #### Endpoint 2: `POST /appstle/verify` **When is this called?** Appstle calls this when a merchant initiates the connection from your app's dashboard (Flow A). Appstle is asking your app: "Did you actually send this nonce?" **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f6...the-nonce-you-generated" } ``` | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------- | | `shop_domain` | string | The merchant's Shopify domain | | `callback_nonce` | string | The nonce your app originally sent in the `/connect` call | **What should your app do?** 1. Look up the stored nonce for this `shop_domain` 2. Compare the `callback_nonce` from the request against your stored nonce 3. If they match: delete the stored nonce (it's single-use) and return `{ "verified": true }` 4. If they don't match: return `{ "verified": false }` ```javascript Node.js (Express) theme={null} router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; // 1. Look up the stored nonce for this shop const isValid = await verifyAndDeleteNonce(shop_domain, callback_nonce); // 2. Return the result res.json({ verified: isValid }); }); ``` ### Step 4: Implement the connect flow (your dashboard) Now build the merchant-facing "Connect Appstle Loyalty" button in your app's dashboard. #### Partner-initiated connect (Flow A) — step by step This is the flow where the merchant clicks "Connect Appstle" in your dashboard. The merchant initiates the connection from inside your app's UI. Store it keyed by `shop_domain` with a 5-minute TTL. Send the `shop_domain`, the `callback_nonce`, and your Partner Secret. Appstle also confirms the shop has Appstle Loyalty installed. Payload: the `shop_domain` and the same `callback_nonce`. Confirm it matches, delete it, return `{ "verified": true }`. The connection is pending — no access token has been issued yet. They open **Settings → Partner Connections** and click **Approve**. The token is POSTed to YOUR `/appstle/approved` endpoint. Show "Connected!" to the merchant. You're done. **Full implementation (Node.js):** ```javascript theme={null} const crypto = require('crypto'); const axios = require('axios'); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL; // https://loyalty-admin.appstle.com // Called when merchant clicks "Connect Appstle" in your dashboard async function connectToAppstle(shopDomain) { // Step 2: Generate a cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Store it so your /appstle/verify endpoint can look it up later await storeNonce(shopDomain, nonce); // see nonce storage examples above // Step 3: Call Appstle's partner connect endpoint const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json', }, } ); // Steps 4-6 happen automatically (Appstle calls your /appstle/verify) // Step 7: Appstle returns pending status — token is NOT delivered yet const { status } = response.data; if (status === 'pending_merchant_approval') { // The merchant needs to approve in their Appstle dashboard. // Once approved, Appstle will POST the token to your /appstle/approved endpoint. await markConnectionPending(shopDomain); return { pending: true }; } throw new Error('Connection failed'); } ``` **curl equivalent:** ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/partner/search-pie/connect" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890" }' ``` **Success response:** ```json theme={null} { "status": "pending_merchant_approval" } ``` **What happens next?** The merchant will see a "Pending Request" in their Appstle dashboard under **Settings → Partner Connections**. (This menu appears automatically once a partner initiates a connection request — it is not visible before any partner has connected.) When they click "Approve," Appstle creates a scoped API key and delivers it to your `/appstle/approved` endpoint (see [Handling the approval callback](#handling-the-approval-callback) below). Pending requests expire after 30 days if not acted on. **Deep link to approval screen:** You can redirect the merchant directly to the approval screen to minimize friction: ``` https://admin.shopify.com/store/{shop-handle}/apps/appstle-loyalty/dashboard/partner-connections ``` Replace `{shop-handle}` with the merchant's store handle (the part before `.myshopify.com`). This takes them straight to the pending connection for one-click approval. You can trigger this redirect in your UI immediately after receiving the `pending_merchant_approval` response. #### Appstle-initiated connect (Flow B) — step by step This is the flow where the merchant clicks "Connect" in Appstle's dashboard. The connection is initiated from inside Appstle, not your UI. Internal Appstle call — your app is not involved yet. Payload: `shop_domain`, `app`, `callback_url`, and `callback_nonce`. Persist them keyed by `shop_domain` for the verify step. Send `shop_domain`, `callback_nonce`, and your Partner Secret. The `access_token` is returned in the response body. Connection complete. ##### Completing the handshake (Flow B) After your `/appstle/connect` endpoint receives the nonce and callback URL, your app completes the connection by calling Appstle's verify endpoint: ```bash theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/partner/search-pie/verify" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "the-nonce-appstle-sent-in-the-connect-call" }' ``` **Success response:** ```json theme={null} { "verified": true, "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Failed response (nonce expired or mismatched):** ```json theme={null} { "verified": false } ``` You must call the verify endpoint within 5 minutes of receiving the nonce. After that, the nonce expires and the merchant will need to try again. ## Using the API token After a successful connection, your app has an `access_token` (prefixed with `apst_`). For Appstle-initiated connections (Flow B), the token is returned immediately in the verify response. For partner-initiated connections (Flow A), the token is delivered asynchronously to your `/appstle/approved` endpoint after the merchant approves (see [Handling the approval callback](#handling-the-approval-callback) below). Use this token exactly like a merchant API key — pass it in the `X-API-Key` header: ```bash theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=cool-store.myshopify.com&customer_id=12345" \ -H "X-API-Key: apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" ``` ### Token properties | Property | Detail | | -------------- | ------------------------------------------------------------------------------------------ | | **Format** | Starts with `apst_` followed by 40 alphanumeric characters | | **Scope** | One token per merchant per partner | | **Permission** | `READ_ONLY` or `READ_WRITE` (set during partner onboarding) | | **Billing** | Partner tokens bypass the paid API plan — merchants are never billed for partner API usage | | **Revocation** | Revoked instantly when the merchant disconnects or uninstalls Appstle | | **Expiry** | Tokens do not expire on their own. They remain valid until explicitly revoked. | ### Available endpoints Partner tokens grant access to the same External API endpoints as merchant API keys: * **Customer loyalty data** — `GET /api/external/customer-loyalty` * **Point transactions** — `GET /api/external/point-transaction-history/{customerId}` * **Add points** — `POST /api/external/add-points` *(requires READ\_WRITE)* * **Redeem points** — `POST /api/external/redeem-points` *(requires READ\_WRITE)* * **Add store credits** — `POST /api/external/add-credits` *(requires READ\_WRITE)* * **Enroll customer** — `POST /api/external/enroll-customer` *(requires READ\_WRITE)* * And all other `/api/external/*` endpoints See the full [Integration guide](/loyalty/integration-guide) for complete endpoint documentation. ## Data sync (push model) Some integrations work best when Appstle pushes data to your app, rather than your app pulling from Appstle's API. For example, a search platform might need Appstle to push customer loyalty data so it can be indexed alongside other store data. ### How it works During onboarding, you can configure a `sync_path` on your server (e.g., `/appstle/sync`). When loyalty events occur (points earned, tier changes, etc.), Appstle calls your endpoint with the relevant data. | Config field | Example | Description | | ----------------- | --------------------- | ------------------------------------------------ | | `sync_path` | `/appstle/sync` | Your endpoint where Appstle pushes loyalty data | | `disconnect_path` | `/appstle/disconnect` | Your endpoint called when a merchant disconnects | ### Authentication When Appstle calls your endpoints, it authenticates using the auth mode configured for your partner: * **Partner Secret mode:** no additional headers (your endpoints are responsible for validating the source — consider IP allowlisting) * **HMAC-SHA256 mode (recommended):** Appstle signs every request with `X-Partner-Timestamp` and `X-Partner-Signature` headers. Your app should verify the HMAC signature to confirm the request came from Appstle. **Verifying incoming HMAC signatures (Node.js):** ```javascript theme={null} const crypto = require('crypto'); function verifyAppstleSignature(req, hmacKey) { const timestamp = req.headers['x-partner-timestamp']; const signature = req.headers['x-partner-signature']; if (!timestamp || !signature) return false; // Reject if timestamp is more than 5 minutes old const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) return false; // Compute expected signature const data = timestamp + req.rawBody; // make sure you capture raw body const expected = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); // Constant-time comparison return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ### Pull vs push — which model do I need? | Model | Connect mode | Your app calls Appstle? | Appstle calls your app? | Use case | | ------------------ | ---------------------------- | ------------------------- | --------------------------------- | ------------------------------------------------- | | **Pull** (default) | Nonce Handshake | Yes (via `apst_` API key) | No | Helpdesks, CRMs reading loyalty data on demand | | **Push** | Simple Token Exchange | No | Yes (via your token + sync\_path) | Search platforms, analytics tools that index data | | **Bidirectional** | Nonce Handshake + sync\_path | Yes | Yes | Full two-way integrations | ## Disconnecting ### Merchant disconnects from Appstle Merchants can disconnect your integration anytime from **Appstle Dashboard → Settings → Partner Connections**. When they do: * Your API token for that merchant is revoked immediately * Subsequent API calls will return `401 Unauthorized` * Appstle sends a disconnect webhook to your app (if you configured a `disconnect_path` during onboarding — see below) * Your app should handle this gracefully and show a "Reconnect" option **Best practice:** in your API client, check for `401` responses and update your UI to show the connection as disconnected: ```javascript theme={null} async function callAppstleApi(shopDomain, endpoint) { const token = await getAccessToken(shopDomain); try { const response = await axios.get(`${APPSTLE_BASE}${endpoint}`, { headers: { 'X-API-Key': token }, }); return response.data; } catch (err) { if (err.response?.status === 401) { // Token was revoked — merchant disconnected await markAsDisconnected(shopDomain); throw new Error('Appstle connection was revoked. Merchant needs to reconnect.'); } throw err; } } ``` ### Disconnect webhook (first-class endpoint) Configure a `disconnect_path` during onboarding (defaults to `/appstle/disconnect`). Appstle calls this whenever a merchant disconnects — from the Appstle dashboard, from your app, or by uninstalling Appstle entirely. **You should implement this endpoint for every integration** — it is the only reliable signal that the merchant has revoked access on Appstle's side. Polling `401` responses as a fallback works but lags behind. **Request body from Appstle:** ```json theme={null} { "shop_domain": "cool-store.myshopify.com" } ``` If your partner uses HMAC-SHA256 auth, the webhook includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. **Your endpoint must:** 1. **Look up the connection without filtering on status.** Don't `WHERE status = 'active'` — if the merchant rapid-clicks disconnect twice, the second call may arrive when the row is already inactive. Find by `(shop_domain, partnerId)` only. 2. **Revoke the Appstle access token idempotently.** If the token is already revoked or absent, return success — don't error. Revocation must be safe to call repeatedly. 3. **Mark the local connection inactive.** Clear or null out the stored Appstle token so subsequent API calls don't try to use it. 4. **Return `2xx` even when there was nothing to do.** A no-op disconnect is a successful disconnect from Appstle's perspective. ```javascript Node.js (Express) theme={null} router.post('/appstle/disconnect', async (req, res) => { const { shop_domain } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } // 1. Status-agnostic lookup — don't filter on .where({ status: 'active' }) const connection = await db.connections.findOne({ shopDomain: shop_domain }); if (connection) { // 2. Idempotent token revoke — clearing a null token is a no-op await db.appstleTokens.delete({ shopDomain: shop_domain }); // 3. Mark inactive (upsert-style — safe if already inactive) await db.connections.update( { shopDomain: shop_domain }, { status: 'disconnected', disconnectedAt: new Date() } ); } // 4. Always 2xx — even if nothing was found res.json({ success: true }); }); ``` ```python Python (Flask) theme={null} @app.route("/appstle/disconnect", methods=["POST"]) def appstle_disconnect(): shop_domain = request.json["shop_domain"] # 1. Find without filtering on status connection = Connection.query.filter_by(shop_domain=shop_domain).first() if connection: # 2. Idempotent token revoke AppstleToken.query.filter_by(shop_domain=shop_domain).delete() # 3. Mark inactive (upsert semantics) connection.status = "disconnected" connection.disconnected_at = datetime.utcnow() db.session.commit() # 4. Always 2xx return jsonify({"success": True}) ``` This is a best-effort notification — your app should also handle `401` responses from the Appstle API as a fallback signal that the connection was revoked. ### Partner disconnects programmatically Your app can disconnect a merchant using your partner authentication (Partner Secret or HMAC-SHA256): ```bash With Partner Secret theme={null} curl -X POST "https://loyalty-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Secret: YOUR_PARTNER_SECRET" \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com" }' ``` ```bash With HMAC-SHA256 theme={null} TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://loyalty-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Response:** ```json theme={null} { "success": true } ``` ### Check connection status `GET /api/partner/{partnerId}/status?shop_domain=...` is the **authoritative source of truth** for whether a merchant is connected. If your UI shows a "Connected" badge, derive it from this endpoint — not from whether you happen to have a stored API key locally. **Why this matters:** Older integrations sometimes inferred "connected" from the presence of a per-app API key column in their own database. That column is now a deprecated fallback — it can be stale (key revoked on Appstle's side, your row never updated) and it can't represent `pending_merchant_approval` or `rejected`. Always call `/status` before showing connection state to the merchant or making business decisions based on it. Partners can authenticate with their Partner Secret or HMAC signature: ```bash theme={null} # With Partner Secret curl -X GET "https://loyalty-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Secret: your-partner-secret" # With HMAC curl -X GET "https://loyalty-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" ``` **Response (active connection):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "active", "connected_at": "2026-03-07T20:30:00Z" } ``` **Response (pending merchant approval):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "pending_merchant_approval" } ``` **All possible status values:** | Status | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | Connected and working — API token is valid | | `pending_merchant_approval` | Partner-initiated connect is awaiting merchant approval | | `rejected` | Merchant rejected the connection request | | `expired` | A pending request expired (30-day window) without merchant action — partner must initiate a new connection | | `not_connected` | No connection record exists for this partner + shop, or the connection was previously terminated (by merchant, by partner, or by uninstall). In both cases the token is revoked and your app would need to initiate a new connection. | Use the status endpoint to poll for approval if your app doesn't implement the `/appstle/approved` callback. Poll every 30–60 seconds after initiating a connect. Once the status changes from `pending_merchant_approval` to `active`, your token has been delivered via the approval callback (or you can request it again). ## Handling the approval callback When a merchant approves a partner-initiated connection, Appstle delivers the API token by calling an endpoint on your server. This applies to partner-initiated connections only — Appstle-initiated connections (Flow B) return the token immediately. ### Endpoint: `POST /appstle/approved` The path defaults to `/appstle/approved` but can be customized during onboarding (configured as `approval_callback_path`). Your `/appstle/approved` endpoint **must accept unauthenticated POST requests** from Appstle's servers. Do not put authentication middleware (e.g., JWT validation, API key checks) on this endpoint — Appstle will not send your app's auth credentials when calling this callback. If you need to verify the request is from Appstle, use [HMAC-SHA256 authentication mode](#option-b-hmac-sha256) — when enabled, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) you can verify. **Request body from Appstle (Nonce Handshake mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Request body from Appstle (Simple Token Exchange mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "status": "approved" } ``` In Simple Token Exchange mode, your app already provided its own token during the connect call. The approval callback simply confirms the connection is now active — Appstle will start using your token for API calls. **Expected response:** Return any `2xx` status code with a JSON body (e.g., `{ "success": true }`). If your endpoint returns a non-2xx status (e.g., `401 Unauthorized`), the connection is still approved on Appstle's side, but your app won't know — see [What if the callback fails?](#what-if-the-callback-fails) below. If your partner uses HMAC-SHA256 auth, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. ```javascript Node.js theme={null} router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } if (access_token) { // Nonce Handshake mode — store the Appstle API token await saveToken(shop_domain, access_token); console.log(`Connection approved for ${shop_domain} — token received`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active await markConnectionActive(shop_domain); console.log(`Connection approved for ${shop_domain} — our token is now active`); } res.json({ success: true }); }); ``` ```python Python theme={null} @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active mark_connection_active(shop_domain) return jsonify({"success": True}) ``` ### What if the callback fails? If your endpoint is unreachable or returns an error, the connection is still approved on Appstle's side. The API token exists and is valid. Your app can: 1. **Poll the status endpoint** — check `GET /api/partner/{id}/status?shop_domain=...` until the status is `active` 2. **Retry from Appstle's side** — currently, Appstle does not automatically retry the callback. Contact support if you need the token re-delivered. ### What if the merchant rejects? If the merchant clicks "Reject," the connection status changes to `rejected` and Appstle notifies your app via the disconnect webhook (if configured). Your app should handle this gracefully — show the merchant that the connection was not approved. ## Error handling All partner endpoints return structured error responses: ```json theme={null} { "type": "https://loyalty-admin.appstle.com/problem", "title": "Bad Request", "status": 400, "detail": "UserGeneratedError:Active connection already exists. Disconnect first.", "errorKey": "ALREADY_CONNECTED" } ``` ### Error codes | Error code | HTTP status | When it happens | What to do | | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PARTNER_NOT_FOUND` | 400 | Your Partner ID is wrong, or the partner has been deactivated | Double-check your Partner ID. Contact Appstle if unexpected. | | `TOKEN_INVALID` | 400 | Auth failed: `X-Partner-Secret` is wrong, or HMAC signature is invalid, or timestamp is >5 min off | Verify your secret or HMAC key. Check for trailing whitespace. For HMAC: ensure server clock is synced (NTP) and you're signing `timestamp + body` exactly. | | `SHOP_NOT_FOUND` | 400 | The shop doesn't have Appstle Loyalty installed | Tell the merchant to install Appstle Loyalty first. | | `ALREADY_CONNECTED` | 400 | An active connection already exists for this partner + shop | Call disconnect first, then reconnect. Or skip — you're already connected. | | `VERIFICATION_FAILED` | 400 | Nonce didn't match, expired (>5 min), or your `/verify` endpoint returned `false` | Generate a fresh nonce and try again. Check your nonce storage logic. | | `NOT_CONNECTED` | 400 | Trying to disconnect or check status, but no active connection exists | The merchant may have already disconnected from their side. | | `PARTNER_UNREACHABLE` | 400 | Appstle couldn't reach your `/appstle/connect` or `/appstle/verify` endpoint | Check your endpoint URL is correct, HTTPS, and publicly accessible. Check your server logs. | | `UNEXPECTED_ROLLBACK` | 500 | Your endpoint returned success, but Appstle's transaction was silently rolled back. Manifests in logs as `UnexpectedRollbackException` / `Transaction silently rolled back because it has been marked as rollback-only`. | Common footgun for partners running on transactional frameworks: an inner write throws and gets caught by your handler, but the surrounding transaction has already been marked rollback-only — so the outer commit fails with no visible error from your business logic. Fix is in your code: either let the inner exception propagate, or perform the write in a fresh inner transaction. Don't swallow exceptions inside a transactional boundary. | #### Idempotency requirements — recap The integration framework relies on partners treating callbacks as **at-least-once**. Concretely: * **Connect / approval callbacks:** upsert by `(shop_domain, partnerId)`. Two `/appstle/approved` calls for the same shop must produce the same end state, not two rows. * **Disconnect callback:** find the connection without filtering on status; revoke tokens idempotently; return `2xx` even when there is nothing to do. * **Status reads:** safe by definition — no side effects. If your code is built on "this only ever fires once", expect bugs the first time the merchant flips connect/disconnect quickly or the first time a network blip triggers an Appstle retry. ## Security checklist Before going live, verify all of these: * Partner Secret / HMAC key is stored in environment variables or a secrets manager — not hardcoded in source code * Nonces are generated using a cryptographically secure random generator (`crypto.randomBytes`, `secrets.token_hex`, `SecureRandom`, etc.) * Nonces are stored with a TTL (≤ 5 minutes) and deleted after verification * Nonces are compared using a constant-time comparison to prevent timing attacks (most frameworks do this by default for string equality) * Endpoints are served over HTTPS — Appstle will not call HTTP endpoints * `shop_domain` is validated in your `/appstle/connect` and `/appstle/verify` endpoints — reject domains you don't recognize * Access tokens are stored encrypted at rest (or in a secrets manager) * 401 responses are handled gracefully — show a "Reconnect" option, don't break silently * Error responses from Appstle are logged for debugging * (HMAC only) Server clock is synced via NTP — timestamps more than 5 minutes off will be rejected * (If using disconnect webhook) Your `/appstle/disconnect` endpoint cleans up stored tokens and marks the connection as inactive ## Complete example: partner-initiated flow (Node.js) Here's a full, copy-pasteable implementation of Flow A in Express: ```javascript theme={null} // appstle-partner.js const express = require('express'); const crypto = require('crypto'); const axios = require('axios'); const Redis = require('ioredis'); const router = express.Router(); const redis = new Redis(process.env.REDIS_URL); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL || 'https://loyalty-admin.appstle.com'; const NONCE_TTL = 300; // 5 minutes in seconds // ────────────────────────────────────────────── // Nonce helpers // ────────────────────────────────────────────── async function storeNonce(shopDomain, nonce) { await redis.set(`appstle:nonce:${shopDomain}`, nonce, 'EX', NONCE_TTL); } async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const stored = await redis.get(key); if (!stored || stored !== nonceToCheck) return false; await redis.del(key); return true; } // ────────────────────────────────────────────── // Token storage (use your database in production) // ────────────────────────────────────────────── async function saveToken(shopDomain, accessToken) { // In production: encrypt the token before storing await redis.set(`appstle:token:${shopDomain}`, accessToken); } async function getToken(shopDomain) { return redis.get(`appstle:token:${shopDomain}`); } // ────────────────────────────────────────────── // Flow A: Partner-initiated connect // Called when merchant clicks "Connect Appstle" in YOUR dashboard // ────────────────────────────────────────────── router.post('/connect-appstle', async (req, res) => { const { shopDomain } = req.body; try { // 1. Generate nonce const nonce = crypto.randomBytes(32).toString('hex'); await storeNonce(shopDomain, nonce); // 2. Call Appstle const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' } } ); // 3. Connection is now pending merchant approval if (response.data.status === 'pending_merchant_approval') { // Mark as pending in your system — show the merchant a "waiting for approval" state await redis.set(`appstle:pending:${shopDomain}`, 'true'); return res.json({ pending: true, message: 'Waiting for merchant to approve in Appstle dashboard' }); } res.status(400).json({ error: 'Connection failed' }); } catch (err) { const detail = err.response?.data?.detail || err.message; console.error('Appstle connect failed:', detail); res.status(400).json({ error: detail }); } }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/verify // Called BY Appstle during Flow A to verify your nonce // ────────────────────────────────────────────── router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; const verified = await verifyAndDeleteNonce(shop_domain, callback_nonce); res.json({ verified }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/approved // Called BY Appstle when merchant approves a partner-initiated connection // ────────────────────────────────────────────── router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; if (access_token) { // Nonce Handshake mode — Appstle is delivering our API token await saveToken(shop_domain, access_token); await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Token received for ${shop_domain}`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active on Appstle's side await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Our token is now active for ${shop_domain}`); } res.json({ success: true }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/connect // Called BY Appstle during Flow B (Appstle-initiated) // ────────────────────────────────────────────── router.post('/appstle/connect', async (req, res) => { const { shop_domain, callback_url, callback_nonce } = req.body; // Verify the shop exists in your system // const shop = await db.shops.findOne({ domain: shop_domain }); // if (!shop) return res.status(400).json({ error: 'Unknown shop' }); // Auto-approve: immediately call back to complete the handshake // (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try { const response = await axios.post(callback_url, { shop_domain, callback_nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' }, }); if (response.data.verified && response.data.access_token) { await saveToken(shop_domain, response.data.access_token); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } res.json({ success: true }); }); module.exports = router; ``` ## Complete example: partner-initiated flow (Python) ```python theme={null} # appstle_partner.py import os import secrets import redis import requests from flask import Flask, request, jsonify app = Flask(__name__) r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379")) PARTNER_ID = os.environ["APPSTLE_PARTNER_ID"] PARTNER_SECRET = os.environ["APPSTLE_PARTNER_SECRET"] APPSTLE_BASE = os.environ.get("APPSTLE_BASE_URL", "https://loyalty-admin.appstle.com") NONCE_TTL = 300 # 5 minutes # ── Nonce helpers ── def store_nonce(shop_domain: str, nonce: str): r.set(f"appstle:nonce:{shop_domain}", nonce, ex=NONCE_TTL) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: key = f"appstle:nonce:{shop_domain}" stored = r.get(key) if not stored or stored.decode() != nonce_to_check: return False r.delete(key) return True # ── Token storage ── def save_token(shop_domain: str, access_token: str): r.set(f"appstle:token:{shop_domain}", access_token) # ── Flow A: Partner-initiated connect ── @app.route("/connect-appstle", methods=["POST"]) def connect_appstle(): shop_domain = request.json["shopDomain"] # 1. Generate nonce nonce = secrets.token_hex(32) store_nonce(shop_domain, nonce) # 2. Call Appstle resp = requests.post( f"{APPSTLE_BASE}/api/partner/{PARTNER_ID}/connect", json={"shop_domain": shop_domain, "callback_nonce": nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) resp.raise_for_status() data = resp.json() # 3. Connection is pending merchant approval if data.get("status") == "pending_merchant_approval": r.set(f"appstle:pending:{shop_domain}", "true") return jsonify({"pending": True, "message": "Waiting for merchant to approve in Appstle dashboard"}) return jsonify({"error": "Connection failed"}), 400 # ── Endpoint: POST /appstle/verify (called BY Appstle during Flow A) ── @app.route("/appstle/verify", methods=["POST"]) def appstle_verify(): body = request.json verified = verify_and_delete_nonce(body["shop_domain"], body["callback_nonce"]) return jsonify({"verified": verified}) # ── Endpoint: POST /appstle/approved (called BY Appstle when merchant approves) ── @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active pass # mark connection as active in your DB r.delete(f"appstle:pending:{shop_domain}") return jsonify({"success": True}) # ── Endpoint: POST /appstle/connect (called BY Appstle during Flow B) ── @app.route("/appstle/connect", methods=["POST"]) def appstle_connect(): body = request.json shop_domain = body["shop_domain"] callback_url = body["callback_url"] callback_nonce = body["callback_nonce"] # Auto-approve: call back immediately # (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try: resp = requests.post( callback_url, json={"shop_domain": shop_domain, "callback_nonce": callback_nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) data = resp.json() if data.get("verified") and data.get("access_token"): save_token(shop_domain, data["access_token"]) except Exception as e: app.logger.error(f"Handshake failed: {e}") return jsonify({"success": True}) ``` ## FAQ Yes. Each partner gets its own scoped API token. Merchants can connect as many partners as they want. The tokens are completely independent. All active partner connections for that shop are automatically disconnected. Your tokens will stop working (401 responses). Yes — contact the Appstle team. New connections will use the updated permission, but existing connections keep their original permission until reconnected. Partner tokens share the same rate limits as regular API keys. If you receive a `429 Too Many Requests`, implement exponential backoff. Use a Shopify development store with Appstle Loyalty installed. The partner integration works identically in development and production. You can use a tool like [ngrok](https://ngrok.com) to expose your local endpoints to the internet for testing. The merchant simply needs to click "Connect" again. A new nonce will be generated. Old nonces are automatically cleaned up. Yes. Flow A is for when the merchant connects from your dashboard. Flow B is for when they connect from Appstle's dashboard. Both are needed for a complete integration. You also need the `/appstle/approved` endpoint to receive API tokens after merchant approval (Flow A). No. A new connect handshake requires the merchant to initiate it from one of the dashboards. This is by design — merchants must explicitly authorize each connection. For security and trust. When your app initiates a connection, the merchant hasn't explicitly agreed on Appstle's side. The approval step ensures merchants consciously grant API access to partner apps. Appstle-initiated connections (Flow B) skip this step because the merchant is already clicking "Connect" in the Appstle dashboard. Pending connection requests expire after 30 days. If the merchant doesn't approve or reject within that window, the request expires and your app will need to initiate a new connection. The status changes to `rejected` and your app is notified via the disconnect webhook (if configured). The merchant can be asked to reconnect later if they change their mind — your app can initiate a new connection request. ## Need help? * **Partner onboarding & technical support:** [support@appstle.com](mailto:support@appstle.com) * **Integration guide:** [Third-Party Integration Guide](/loyalty/integration-guide) (for direct API key usage) # Get started with Appstle Loyalty API Source: https://developers.appstle.com/loyalty/quickstart Create an API key, retrieve a customer's loyalty profile with points and VIP tier, then credit points to their account — all in under five minutes. This guide walks you through the three steps needed to make your first Appstle Loyalty API calls: getting a key, reading a customer's loyalty data, and adding points. By the end you will have a working integration you can build on. ## Prerequisites * An active Appstle Loyalty subscription on your Shopify store * Access to the Appstle admin panel * A Shopify customer ID to test with (find one in your Shopify admin under **Customers**) ## Step 1 — Get your API key In your Appstle admin panel, go to **Settings → API Key Management**. Click **Create New Key**. Give it a name like `Quickstart test`, then click **Save**. Copy the displayed key immediately. It is only shown once. Store it as an environment variable: ```bash theme={null} export APPSTLE_API_KEY="apst_your-api-key-here" export SHOP="your-store.myshopify.com" ``` Never put your API key in client-side code. All requests must be made from a server you control. ## Step 2 — Retrieve customer loyalty data Call `GET /api/external/customer-loyalty` with your store domain and a Shopify customer ID. Replace `12345` with a real customer ID from your store. ```bash curl theme={null} curl -X GET \ "https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=${SHOP}&customer_id=12345" \ -H "X-API-Key: ${APPSTLE_API_KEY}" ``` ```javascript Node.js theme={null} const response = await fetch( `https://loyalty-admin.appstle.com/api/external/customer-loyalty?shop=${process.env.SHOP}&customer_id=12345`, { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY } } ); const loyalty = await response.json(); console.log(loyalty); ``` ```python Python theme={null} import httpx, os r = httpx.get( 'https://loyalty-admin.appstle.com/api/external/customer-loyalty', params={'shop': os.environ['SHOP'], 'customer_id': '12345'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) print(r.json()) ``` A successful response looks like this: ```json theme={null} { "availablePoints": 1250, "pendingPoints": 150, "creditedPoints": 1400, "storeCreditBalance": 25.0, "currentVipTier": "Gold", "customerStatus": "ACTIVE" } ``` | Field | Description | | -------------------- | --------------------------------------------- | | `availablePoints` | Points the customer can redeem right now | | `pendingPoints` | Points earned but not yet approved | | `creditedPoints` | Total lifetime points ever credited | | `storeCreditBalance` | Monetary store credit balance | | `currentVipTier` | Active VIP tier name, or empty string if none | | `customerStatus` | `ACTIVE` or `INACTIVE` | If you get a `404`, the customer is not yet enrolled in the loyalty program. See [Step 3 of the integration guide](/loyalty/integration-guide#customer-management) to enroll them first. ## Step 3 — Add points to the customer Call `POST /api/external/add-points` to credit points. Include a `note` so the transaction is clearly labeled in the customer's history. ```bash curl theme={null} curl -X POST \ "https://loyalty-admin.appstle.com/api/external/add-points" \ -H "X-API-Key: ${APPSTLE_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "shop": "your-store.myshopify.com", "customerId": 12345, "points": 100, "note": "Quickstart test credit" }' ``` ```javascript Node.js theme={null} const response = await fetch( 'https://loyalty-admin.appstle.com/api/external/add-points', { method: 'POST', headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ shop: process.env.SHOP, customerId: 12345, points: 100, note: 'Quickstart test credit', }), } ); const result = await response.json(); console.log(result); ``` ```python Python theme={null} import httpx, os, json r = httpx.post( 'https://loyalty-admin.appstle.com/api/external/add-points', headers={ 'X-API-Key': os.environ['APPSTLE_API_KEY'], 'Content-Type': 'application/json', }, content=json.dumps({ 'shop': os.environ['SHOP'], 'customerId': 12345, 'points': 100, 'note': 'Quickstart test credit', }), ) print(r.json()) ``` Call `GET /api/external/customer-loyalty` again and you will see `availablePoints` increased by 100. ## What to build next Every endpoint: point management, customer enrollment, rewards, store credits, and program configuration. Receive real-time notifications for points earned, VIP tier changes, and referral completions. Trigger loyalty actions from any Shopify Flow workflow without writing API code. # Automate loyalty with Shopify Flow Source: https://developers.appstle.com/loyalty/shopify-flow Automate loyalty with Appstle's native Shopify Flow triggers and actions. Award points, assign VIP tiers, and issue store credits — no API code required. Appstle Loyalty integrates natively with Shopify Flow, giving you 8 event triggers and 6 actions. You can automate loyalty workflows entirely within Shopify's no-code automation builder — no webhooks or API calls required. ## Triggers Triggers fire when loyalty events occur in Appstle and pass customer and loyalty data into your Flow workflow. | Trigger | Handle | Description | | ------------------------- | --------------------------- | ---------------------------------------------- | | Loyalty Sign Up | `loyalty-sign-up` | Customer joins the loyalty program | | Loyalty Points Earned | `loyalty-points-earned` | Customer earns loyalty points | | Loyalty Points Redeemed | `loyalty-points-redeemed` | Customer redeems points for a reward | | Loyalty Credits Earned | `loyalty-credits-earned` | Customer earns store credits | | Loyalty VIP Tier Achieved | `loyalty-vip-tier-achieved` | Customer reaches a new VIP tier | | Customer's Birthday | `birthday-trigger` | Triggered on the customer's birth date | | Referral Reward Created | `referral-reward-created` | A reward is created for the referring customer | | Referred Reward Created | `referred-reward-created` | A reward is created for the referred customer | ### Trigger properties All triggers include a `customer_reference` (Shopify customer reference) and a `Note` string describing the activity. The three points and credits triggers (`loyalty-points-earned`, `loyalty-points-redeemed`, `loyalty-credits-earned`) additionally include: * `Points` or `Credits` — the decimal amount earned or redeemed * `Earn Rule ID` or `Redeem Rule ID` — the rule that triggered the activity Every trigger also carries a nested **Customer Loyalty Details** object: | Field | Type | Description | | ---------------------------- | ------- | ------------------------------------ | | `availablePoints` | Float | Current redeemable points balance | | `pendingPoints` | Float | Points awaiting approval | | `creditedPoints` | Float | Total lifetime points earned | | `spentAmount` | Float | Total amount spent by the customer | | `currentVipTier` | String | Active VIP tier name (empty if none) | | `referredCompleted` | Int | Number of completed referrals | | `referralLink` | String | Customer's unique referral URL | | `dob` | String | Date of birth in ISO format (if set) | | `rewardedForFacebook` | Boolean | Facebook like reward earned | | `rewardedForTwitter` | Boolean | X/Twitter follow reward earned | | `rewardedForInstagram` | Boolean | Instagram follow reward earned | | `rewardedForYoutube` | Boolean | YouTube subscribe reward earned | | `rewardedForTiktok` | Boolean | TikTok follow reward earned | | `rewardedForNewsLetter` | Boolean | Newsletter signup reward earned | | `rewardedForSms` | Boolean | SMS signup reward earned | | `rewardedForCreatingAccount` | Boolean | Account creation reward earned | | `rewards` | Array | List of `CustomerReward` objects | Each item in the `rewards` array contains: | Field | Type | Description | | -------------------- | ------------ | --------------------------------------------- | | `description` | String | Reward description | | `discountCode` | String | Generated Shopify discount code | | `status` | RewardStatus | `USED`, `UNUSED`, or `REFUNDED` | | `pointTransactionId` | Int | Internal transaction ID | | `pointRedeemRuleId` | Int | Redemption rule ID | | `orderId` | ID | Shopify order ID (if reward was used) | | `orderName` | String | Order name (e.g., `#1002`) | | `createAt` | String | Creation timestamp | | `usedAt` | String | Timestamp when used (if applicable) | | `expireDate` | String | Expiry timestamp | | `variantId` | ID | Product variant ID (for free product rewards) | The Flow schema exposes a subset of fields from the full customer loyalty object. Fields such as `storeCreditBalance`, `vipTierExpiredAt`, `rewardedForSharingOnFacebook`, and `rewardedForSharingOnX` are present in webhook payloads but are not available in Shopify Flow. ## Actions Actions let your Flow workflows modify loyalty data in Appstle. | Action | Handle | Description | | ------------------------- | --------------------------- | -------------------------------------------- | | Add Points | `add-points` | Award loyalty points to a customer | | Remove Points | `remove-points` | Deduct loyalty points from a customer | | Add Store Credits | `add-store-credits` | Award store credits to a customer | | Remove Store Credits | `remove-store-credits` | Deduct store credits from a customer | | Reward Points for Reviews | `reward-points-for-reviews` | Award points for a product review submission | | Assign VIP Tier | `assign-vip-tier` | Assign or change a customer's VIP tier | ### Add Points | Field | Key | Type | Required | Description | | ------------------- | --------------------- | ------- | -------- | -------------------------------------------------- | | Customer Identifier | `customer-identifier` | String | Yes | Customer ID or email — use `{{customer.email}}` | | Points To Add | `pointsToAdd` | String | No | Number of points to award | | Rule Id | `rule-id` | Integer | No | Static rule ID from Appstle Loyalty | | Reason | `reason` | String | Yes | Shown to the customer in their transaction history | The `points` (Decimal) field is deprecated. Use `pointsToAdd` instead. If you see the old field in an existing action, remove and re-add the action block to get the current version. ### Remove Points | Field | Key | Type | Required | Description | | ---------------- | ---------------- | -------------------- | -------- | -------------------------- | | Customer | — | `customer_reference` | Yes | Shopify customer reference | | Points To Remove | `pointsToRemove` | String | Yes | Number of points to deduct | | Reason | `reason` | String | Yes | Shown to the customer | ### Add Store Credits | Field | Key | Type | Required | Description | | -------------------- | ------------------- | -------------------- | -------- | -------------------------- | | Customer | — | `customer_reference` | Yes | Shopify customer reference | | Store Credits To Add | `storeCreditsToAdd` | String | Yes | Amount in store currency | | Reason | `reason` | String | Yes | Shown to the customer | ### Remove Store Credits | Field | Key | Type | Required | Description | | ----------------------- | ---------------------- | -------------------- | -------- | -------------------------- | | Customer | — | `customer_reference` | Yes | Shopify customer reference | | Store Credits To Remove | `storeCreditsToRemove` | String | Yes | Amount to deduct | | Reason | `reason` | String | Yes | Shown to the customer | ### Reward Points for Reviews Use this action to automatically award points when a review is submitted through a supported platform. | Field | Key | Type | Required | Description | | -------------- | ---------------- | ------ | -------- | ------------------------------------------------- | | Customer Email | `customer-email` | String | Yes | Use the customer email variable from your trigger | | Product ID | `product-id` | String | Yes | Product ID variable from your trigger | | Rating | `review-rating` | String | Yes | Review rating value | | Review Type | `review-type` | String | Yes | Platform identifier (see below) | | Images Count | `image-count` | String | No | Number of images attached to the review | | Videos Count | `video-count` | String | No | Number of videos attached to the review | Supported `review-type` values: ``` REVIEWS_IO AIR_REVIEWS OKENDO LEAVE_REVIEW_LOOX_IO LEAVE_REVIEW_STAMPED_IO LEAVE_REVIEW_PRODUCT_REVIEWS ``` ### Assign VIP Tier | Field | Key | Type | Required | Description | | ------------------- | --------------------- | ------ | -------- | --------------------------------------------------------------------------------------------- | | Customer Identifier | `customer-identifier` | String | Yes | Customer ID or email | | VIP Tier Name | `vip-tier-name` | String | Yes | Exact name of the tier as configured in Appstle | | Lock Tier | `lock-tier` | String | No | Set to `"true"` to prevent automatic recalculation from changing the tier. Default `"false"`. | | Reason | `reason` | String | No | Reason recorded in the audit log | The tier name must exactly match a VIP tier configured in your Appstle Loyalty settings — for example, `"Gold"` or `"Silver"`. If the customer is already on the specified tier, no changes are made. ## Pre-built Flow templates Appstle provides ready-to-use Flow templates for common review platform integrations: | Template | Review platform | | ------------------------- | --------------- | | Points for Reviews.io | Reviews.io | | Points for Okendo Reviews | Okendo | | Points for Loox Reviews | Loox | | Points for Air Reviews | Air Reviews | | Points for Rivyo Reviews | Rivyo | Install a template from the Shopify Flow template library and it will be pre-configured with the correct action fields for that platform. ## Example workflows ``` Trigger: Loyalty Sign Up ↓ Action: Send email marketing Customer Email: {{customer.email}} Subject: "Welcome to our loyalty program" ``` ``` Trigger: Loyalty VIP Tier Achieved Condition: currentVipTier equals "Gold" ↓ Action: Add customer tags Tags: "vip-gold" ``` ``` Trigger: Customer's Birthday ↓ Action: Add Points Customer Identifier: {{customer.email}} Points To Add: 100 Reason: "Happy Birthday bonus" ``` ``` Trigger: Any trigger or condition ↓ Action: Assign VIP Tier Customer Identifier: {{customer.email}} VIP Tier Name: "Gold" Reason: "Upgraded via Shopify Flow" ``` ``` Trigger: Okendo review submitted (via Okendo Flow trigger) ↓ Action: Reward Points for Reviews Customer Email: {{customer.email}} Product ID: {{product.id}} Rating: {{review.rating}} Review Type: "OKENDO" ``` # Appstle Loyalty webhook events and setup Source: https://developers.appstle.com/loyalty/webhooks Set up Appstle Loyalty webhooks to receive real-time notifications for point transactions, VIP tier changes, and referrals — with Svix signature verification. Appstle Loyalty webhooks deliver real-time HTTP POST notifications to your endpoint whenever a loyalty event occurs. The webhook infrastructure is powered by [Svix](https://www.svix.com/), which provides automatic retries, cryptographic signature verification, and detailed delivery logs. Webhooks are available on paid plans. Contact [support@appstle.com](mailto:support@appstle.com) to enable webhook access on your account. ## Setting up an endpoint In your Appstle Loyalty admin, go to **Settings → Webhooks**. Click **Add Endpoint** and enter your publicly accessible HTTPS URL. Your endpoint must be reachable from the internet — localhost URLs will not work. Select the event types you want to subscribe to, or subscribe to all events. Click **Save**. Your endpoint will start receiving events immediately. Your endpoint must return a `2xx` status code within the timeout window. Return `200 OK` immediately and process the event asynchronously to avoid timeouts under load. ## How delivery works Each webhook is an HTTP POST request with a JSON body. Svix handles delivery with: * Automatic retries with exponential backoff (5 attempts over 3 days on failure) * Unique message IDs for idempotency * Signed request headers for verification * Delivery logs and manual replay from **Settings → Webhooks → Message Logs** ## Event types | Event type | Description | | --------------------------- | ----------------------------------------------------- | | `loyalty.sign-up` | Customer joined the loyalty program | | `loyalty.earned` | Customer earned points for any activity | | `loyalty.redeemed` | Customer redeemed points for a reward | | `loyalty.credits-earned` | Customer earned store credits | | `loyalty.vip-tier-achieved` | Customer reached a new VIP tier | | `loyalty.birthday-trigger` | Customer's birthday reward was issued | | `loyalty.referral-reward` | Referring customer received a referral reward | | `loyalty.referred-reward` | Newly referred customer received their welcome reward | ## Payload structure All events follow the same envelope: ```json theme={null} { "type": "loyalty.earned", "data": { // event-specific payload } } ``` ### Common payload fields Every event's `data` object includes these fields: | Field | Type | Description | | ------------------------ | ------ | ---------------------------------------------------------- | | `customerId` | Number | Shopify customer ID | | `customerEmail` | String | Customer's email address | | `note` | String | Optional description of the event | | `points` | Number | Points involved (earned or redeemed amount) | | `earnRuleId` | Number | Earn rule ID that triggered points (`loyalty.earned` only) | | `redeemRuleId` | Number | Redemption rule ID used (`loyalty.redeemed` only) | | `webhookEventType` | String | Internal event type name | | `customerLoyaltyDetails` | Object | Full loyalty profile snapshot at the time of the event | ### `customerLoyaltyDetails` fields | Field | Type | Description | | ------------------------------ | -------- | ------------------------------------------------ | | `availablePoints` | Number | Current redeemable points balance | | `pendingPoints` | Number | Points awaiting approval | | `creditedPoints` | Number | Total lifetime points earned | | `spentAmount` | Number | Total amount spent by this customer | | `storeCreditBalance` | Number | Current store credit balance | | `currentVipTier` | String | Customer's current VIP tier name (empty if none) | | `vipTierExpiredAt` | DateTime | When the VIP tier expires (if applicable) | | `referralLink` | String | Customer's unique referral URL | | `referredCompleted` | Number | Number of completed referrals | | `dob` | Date | Date of birth in ISO 8601 format (if set) | | `rewards` | Array | Active and past reward objects | | `rewardedForFacebook` | Boolean | Facebook follow reward earned | | `rewardedForTwitter` | Boolean | X/Twitter follow reward earned | | `rewardedForInstagram` | Boolean | Instagram follow reward earned | | `rewardedForYoutube` | Boolean | YouTube subscribe reward earned | | `rewardedForTiktok` | Boolean | TikTok follow reward earned | | `rewardedForNewsLetter` | Boolean | Newsletter signup reward earned | | `rewardedForSms` | Boolean | SMS signup reward earned | | `rewardedForCreatingAccount` | Boolean | Account creation reward earned | | `rewardedForSharingOnFacebook` | Boolean | Facebook share reward earned | | `rewardedForSharingOnX` | Boolean | X/Twitter share reward earned | Webhook payloads include additional fields (such as `storeCreditBalance`, `vipTierExpiredAt`, `rewardedForSharingOnFacebook`, and `rewardedForSharingOnX`) that are not available in the Shopify Flow GraphQL schema. Flow receives a subset of these fields. Each item in the `rewards` array contains: | Field | Type | Description | | -------------------- | -------- | ---------------------------------------------- | | `description` | String | Reward description | | `discountCode` | String | Generated Shopify discount code | | `status` | String | `UNUSED`, `USED`, or `REFUNDED` | | `pointTransactionId` | Number | Point transaction ID | | `pointRedeemRuleId` | Number | Redemption rule ID used | | `orderId` | String | Shopify order GID where reward was used | | `orderName` | String | Order name (e.g., `#1002`) | | `createAt` | DateTime | When the reward was created | | `usedAt` | DateTime | When the reward was used (if applicable) | | `expireDate` | DateTime | When the reward expires | | `variantId` | String | Shopify variant GID (for free product rewards) | ## Example payloads ```json theme={null} { "type": "loyalty.sign-up", "data": { "customerId": 12345, "customerEmail": "member@example.com", "note": "Welcome bonus applied", "points": 100, "earnRuleId": null, "redeemRuleId": null, "customerLoyaltyDetails": { "availablePoints": 100, "pendingPoints": 0, "creditedPoints": 100, "spentAmount": 0, "storeCreditBalance": 0, "currentVipTier": "", "referralLink": "https://your-store.myshopify.com?ref=abc123", "referredCompleted": 0, "rewards": [], "rewardedForCreatingAccount": true } } } ``` ```json theme={null} { "type": "loyalty.earned", "data": { "customerId": 12345, "customerEmail": "member@example.com", "note": "Purchase reward", "points": 250, "earnRuleId": 7, "redeemRuleId": null, "customerLoyaltyDetails": { "availablePoints": 850, "pendingPoints": 0, "creditedPoints": 1100, "spentAmount": 320.00, "currentVipTier": "Silver", "referralLink": "https://your-store.myshopify.com?ref=abc123", "referredCompleted": 2, "rewards": [] } } } ``` ```json theme={null} { "type": "loyalty.redeemed", "data": { "customerId": 12345, "customerEmail": "member@example.com", "note": null, "points": 500, "earnRuleId": null, "redeemRuleId": 3, "customerLoyaltyDetails": { "availablePoints": 350, "creditedPoints": 1100, "currentVipTier": "Silver", "rewards": [ { "description": "$5 off your next order", "discountCode": "REWARD-XXXXX", "status": "UNUSED", "pointRedeemRuleId": 3, "createAt": "2026-02-15T10:30:00Z", "expireDate": "2026-05-15T00:00:00Z" } ] } } } ``` ```json theme={null} { "type": "loyalty.vip-tier-achieved", "data": { "customerId": 12345, "customerEmail": "member@example.com", "note": "Reached Gold tier", "points": 0, "customerLoyaltyDetails": { "availablePoints": 2100, "creditedPoints": 5000, "spentAmount": 1250.00, "currentVipTier": "Gold", "vipTierExpiredAt": "2027-01-01T00:00:00Z" } } } ``` ## Signature verification Every webhook request is signed by Svix. Always verify the signature before processing. Svix includes three headers on every request: | Header | Description | | ---------------- | --------------------------------------------- | | `svix-id` | Unique message ID — use as an idempotency key | | `svix-timestamp` | Unix timestamp of delivery | | `svix-signature` | HMAC-SHA256 signature | Find your **webhook signing secret** in your Appstle dashboard under **Settings → Webhooks → \[your endpoint]**. ```javascript Node.js theme={null} const { Webhook } = require('svix'); const secret = 'whsec_your_signing_secret'; app.post('/webhooks/appstle-loyalty', express.raw({ type: 'application/json' }), (req, res) => { const wh = new Webhook(secret); let event; try { event = wh.verify(req.body, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }); } catch (err) { return res.status(400).send('Signature verification failed'); } const { customerId, points, customerLoyaltyDetails } = event.data; switch (event.type) { case 'loyalty.vip-tier-achieved': // Send VIP welcome email, add Shopify customer tag break; case 'loyalty.referral-reward': // Notify referrer of their reward break; case 'loyalty.earned': // Sync points balance to CRM break; } res.status(200).send('OK'); }); ``` ```python Python theme={null} from svix.webhooks import Webhook, WebhookVerificationError secret = "whsec_your_signing_secret" @app.route('/webhooks/appstle-loyalty', methods=['POST']) def webhook(): try: wh = Webhook(secret) event = wh.verify(request.data, { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), }) except WebhookVerificationError: return "Verification failed", 400 if event["type"] == "loyalty.vip-tier-achieved": tier = event["data"]["customerLoyaltyDetails"]["currentVipTier"] # handle tier upgrade return "OK", 200 ``` Pass the **raw request body** to the verification function before JSON parsing. Parsing the body first alters the byte representation and will cause verification to fail. For Go, Ruby, PHP, Java, and C# examples, see the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how). ## Idempotency Webhooks may be delivered more than once due to network conditions or retries. Use the `svix-id` header as an idempotency key to safely deduplicate events in your handler. ## Retry schedule If your endpoint returns a non-`2xx` response or times out, Svix retries with exponential backoff across 5 attempts over 3 days. View delivery attempts and replay individual events from **Settings → Webhooks → Message Logs**. ## Local development Use a tunneling tool such as ngrok to expose your local server during development: ```bash theme={null} ngrok http 3000 # Add https://your-id.ngrok.io/webhooks/appstle-loyalty as your endpoint in the dashboard ``` ## Troubleshooting | Issue | Solution | | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | | Signature verification fails | Use the raw request body before JSON parsing. Confirm you are using the correct secret from the dashboard. | | Not receiving events | Confirm webhooks are enabled under Settings and your account plan includes webhook access. | | Endpoint timing out | Return `200 OK` immediately and process events in a background job or queue. | | Duplicate events | Use the `svix-id` header as an idempotency key and skip already-processed IDs. | # Get paginated past or upcoming orders report Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/get-paginated-past-or-upcoming-orders-report /memberships/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/past-orders/report Retrieves a paginated list of membership billing attempts filtered by status (SUCCESS, FAILURE, QUEUED, SKIPPED) with optional contract filtering. **Key Features:** - **Status Filtering**: Filter by SUCCESS (past successful orders), FAILURE (failed billing attempts), QUEUED (upcoming scheduled orders), or SKIPPED (intentionally skipped orders) - **Contract Filtering**: Optional filtering by specific membership contract ID - **Pagination Support**: Returns paginated results with page headers - **Order History**: Access complete billing attempt history **Status Values:** - `SUCCESS`: Completed successful billing attempts (past orders) - `FAILURE`: Failed billing attempts that need attention - `QUEUED`: Upcoming scheduled billing attempts (future orders) - `SKIPPED`: Manually or automatically skipped billing attempts **Use Cases:** - Generate customer order history reports - Display upcoming scheduled deliveries - Track failed billing attempts for recovery - Export membership order data for analysis - Monitor skipped orders and cancellations **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get past orders Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/get-past-orders /memberships/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/past-orders Retrieves historical billing attempts including successful, failed, and skipped orders. Returns completed billing attempts for reporting and history display. **Query Options:** - No params: All past orders for shop - contractId: Order history for specific contract - customerId: Order history for specific customer **Use Cases:** - Display order history in customer portal - Generate billing reports - Track payment success/failure rates - Customer service order lookup **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get upcoming orders Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/get-upcoming-orders /memberships/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/top-orders Retrieves upcoming/scheduled billing attempts for shop, contract, or customer. Returns future orders that haven't been processed yet. **Query Options:** - No params: All upcoming orders for shop - contractId: Upcoming orders for specific contract - customerId: Upcoming orders for specific customer **Use Cases:** - Display next delivery dates in portal - Show upcoming charges - Allow order modifications before processing **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Retry billing for failed attempt Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/retry-billing-for-failed-attempt /memberships/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/attempt-billing/{id} Immediately triggers a billing attempt for a membership contract, bypassing the normal scheduled billing time. This endpoint processes the billing asynchronously via Shopify's Subscription Billing API, creating an order and charging the customer's payment method. **How Billing Retry Works:** 1. **Validation**: Checks if shop has `enableImmediatePlaceOrder` permission (premium feature) 2. **Async Trigger**: Queues billing job in background (returns immediately) 3. **Shopify API Call**: Sends `subscriptionBillingAttemptCreate` mutation to Shopify 4. **Payment Processing**: Charges customer's stored payment method 5. **Order Creation**: Creates Shopify order if payment succeeds 6. **Notification**: Sends order confirmation email to customer (if enabled) 7. **Webhook Events**: Triggers `SUBSCRIPTION_BILLING_ATTEMPTS_SUCCESS` or `_FAILURE` webhook **When to Use This Endpoint:** - **Payment Failed Previously**: Customer updated credit card, retry failed billing - **Scheduled Too Far Out**: Customer wants early delivery, bill before scheduled date - **Payment Method Updated**: Customer just added new payment method, retry immediately - **Manual Recovery**: Merchant intervention to recover failed membership - **Testing/QA**: Validate membership billing flow in test environments - **Customer Request**: Expedited order fulfillment for urgent needs **Important Limitations & Restrictions:** - **Permission Required**: Only available to shops with `enableImmediatePlaceOrder` permission - This is a premium feature - free plans will receive 400 error - Upgrade membership plan in Appstle admin to enable - **Rate Limiting**: Maximum 5 billing attempts per contract per hour (anti-abuse) - **Billing Attempt Status**: Can retry attempts with status `QUEUED`, `SCHEDULED`, or `FAILED` - **Cannot Retry**: Billing attempts with status `SUCCESS` (order already created) - **Duplicate Prevention**: Shopify prevents duplicate orders within 24-hour window - **Payment Method Required**: Contract must have valid payment method attached **Asynchronous Processing Details:** This endpoint returns **immediately** (200 OK) before billing completes. Actual billing happens in background: - **Success Response**: Means billing job was queued, NOT that payment succeeded - **Actual Result**: Check via webhooks or poll `/subscription-billing-attempts` endpoint - **Processing Time**: Typically 5-30 seconds depending on Shopify API response time - **Timeout Handling**: Background job retries up to 3 times if Shopify API times out **Checking Billing Result (Recommended Flow):** ``` Step 1: Call this endpoint to trigger billing PUT /subscription-billing-attempts/attempt-billing/123456 Response: 200 OK (billing queued) Step 2: Wait 10-15 seconds for processing Step 3: Poll billing attempt status GET /subscription-billing-attempts?id=123456 Check 'status' field: - SUCCESS: Order created, payment captured - FAILED: Payment declined or error occurred - PROCESSING: Still in progress, poll again Step 4: (Alternative) Use webhooks for real-time updates Configure webhook: SUBSCRIPTION_BILLING_ATTEMPTS_SUCCESS Receive notification when billing completes ``` **Common Error Scenarios:** - **400 - Permission Denied**: "You don't have permission to place immediate orders" - **Solution**: Upgrade to premium plan with `enableImmediatePlaceOrder` feature - **400 - Rate Limit**: "Too many billing attempts. Maximum 5 per hour per contract." - **Solution**: Wait before retrying, investigate why billing keeps failing - **400 - Invalid Status**: "Cannot retry billing attempt with status SUCCESS" - **Solution**: Order already created successfully, check order history - **404 - Not Found**: "Billing attempt not found" - **Solution**: Verify billing attempt ID belongs to this shop - **500 - Shopify API Error**: Background job may fail if Shopify API is down - **Solution**: Retry after 5 minutes, check Shopify status page **Payment Failure Reasons (Check After Async Processing):** After billing processes, if status becomes `FAILED`, common reasons include: - **Card Declined**: Insufficient funds, expired card, fraud detection - **Invalid Payment Method**: Customer deleted payment method - **3D Secure Failed**: Authentication challenge not completed - **Gateway Error**: Payment processor (Stripe/Shopify Payments) issue - **Inventory Shortage**: Product out of stock, cannot fulfill order - **Membership Canceled**: Contract canceled between retry trigger and processing **Integration Best Practices:** 1. **Always Check Permission First**: Call `/shop-info` or similar to verify feature access 2. **Implement Webhooks**: Don't rely on polling - use webhooks for real-time status 3. **User Communication**: Warn user "Billing in progress..." since it's async 4. **Idempotency**: Safe to call multiple times - Shopify prevents duplicate orders 5. **Error Handling**: Gracefully handle permission errors for free plan users 6. **Logging**: Track billing attempt ID for troubleshooting and support **Example Use Case - Customer Portal:** ``` Scenario: Customer's card declined, they updated payment method 1. Customer sees "Payment Failed" in portal 2. Customer clicks "Update Payment Method" → adds new card 3. Customer clicks "Retry Payment Now" button 4. Frontend calls: PUT /subscription-billing-attempts/attempt-billing/789 5. Display: "Processing payment..." spinner 6. Poll endpoint every 5 seconds for status update 7. On SUCCESS: Show "Payment successful! Order #12345 created." 8. On FAILED: Show decline reason + retry instructions ``` **Difference from Skip/Reschedule:** - **Attempt Billing**: Immediately charges customer and creates order NOW - **Skip**: Postpones order to next billing cycle, no charge - **Reschedule**: Changes billing date without triggering immediate charge **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter. Additionally requires merchant account to have `enableImmediatePlaceOrder` permission (premium feature). # Skip upcoming membership order Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/skip-upcoming-membership-order /memberships/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/skip-order/{id} Skips a scheduled billing attempt, preventing the order from being processed. The next billing date is automatically recalculated based on the membership frequency. **Key Features:** - **Flexible Skipping**: Skip any upcoming order - **Auto-Rescheduling**: Next billing date automatically adjusted - **Prepaid Support**: Handles both regular and prepaid memberships - **Activity Logging**: Tracks skip events for reporting - **Customer Control**: Allow members to manage delivery timing **How It Works:** 1. Marks the billing attempt as skipped 2. Calculates new next billing date (current date + frequency) 3. Updates membership contract in Shopify 4. Logs skip activity with event source 5. Returns updated billing attempt details **Use Cases:** - Customer going on vacation - Too much inventory on hand - Temporary pause without canceling - Budget constraints for specific month - Customize delivery schedule **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update membership billing attempt Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/update-membership-billing-attempt /memberships/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts Updates billing attempt details for a membership contract. Billing attempts represent individual charge attempts for recurring membership orders. **Key Information Updated:** - **Billing Date**: Next scheduled billing/charge date - **Status**: Success, failed, pending, or scheduled - **Error Messages**: Failure reasons for declined payments - **Retry Count**: Number of retry attempts made - **Order ID**: Associated Shopify order if billing succeeded - **Amount**: Billing amount charged or attempted **Billing Attempt Lifecycle:** 1. **Scheduled**: Billing attempt is queued for future processing 2. **Pending**: Charge is being processed by payment gateway 3. **Success**: Payment captured, order created 4. **Failed**: Payment declined or error occurred 5. **Retrying**: Automatic retry scheduled after failure **Common Use Cases:** - Reschedule failed billing attempts to a new date - Update billing date to align with customer preferences - Mark manual payment reconciliation in external systems - Sync billing status with external payment processors - Trigger retry logic for failed payment attempts - Update billing metadata for reporting and analytics **Important Notes:** - Changing billing date will affect the membership's billing cycle - Only pending or failed attempts can typically be modified - Successfully billed attempts are immutable in most cases **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update order note for billing attempt Source: https://developers.appstle.com/memberships-admin-api/billing-&-orders/update-order-note-for-billing-attempt /memberships/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts-update-order-note/{id} Updates the order note/instructions for a specific upcoming billing attempt. These notes are stored with the billing attempt and will appear on the Shopify order when it's created. Order notes are visible to merchants in Shopify admin and can be printed on packing slips. **How It Works:** 1. Accepts a billing attempt ID and new order note text 2. Updates the billing attempt record immediately 3. Note will be included when the order is created during billing 4. Previous order note (if any) is completely replaced 5. Empty string will clear the existing order note **Important Timing Considerations:** - Can only update billing attempts with status: `QUEUED` or `SCHEDULED` - Cannot update billing attempts that have already been processed (`SUCCESS` or `FAILED`) - Changes apply to the next billing cycle only (does not affect past orders) - For recurring notes across all future orders, use membership contract order notes instead **Character Limits & Validation:** - **Maximum Length**: 5000 characters (Shopify's order note limit) - **Encoding**: Supports UTF-8 (emojis, international characters allowed) - **HTML**: Not rendered - plain text only, HTML tags will display as text - **Line Breaks**: Preserved using `\n` characters - **Special Characters**: Quotes, apostrophes automatically escaped **Common Use Cases:** - **Delivery Instructions**: "Please leave package at side door" or "Ring doorbell twice" - **Special Handling**: "Fragile items - handle with care" or "Refrigerate immediately" - **Gift Messages**: "Happy Birthday! Love, Sarah" (for gift memberships) - **Custom Requests**: "Include extra ice packs" or "No substitutions please" - **One-Time Changes**: "Skip broccoli this week, double the carrots instead" - **Fulfillment Notes**: "Use expedited shipping" or "Pack items separately" **Order Note vs Contract Note:** - **Billing Attempt Note** (this endpoint): Applies to ONE specific upcoming order only - **Contract Note** (`/subscription-contracts-update-order-note`): Applies to ALL future orders - If both exist, they are concatenated in the final Shopify order **Example Workflows:** ``` Scenario 1: One-time delivery instruction 1. Customer going on vacation next week 2. Call this endpoint to add "Deliver to neighbor at #123" for next billing attempt 3. Following orders resume normal delivery (no note) Scenario 2: Clearing unwanted notes 1. Previous note says "Call before delivery" 2. Customer requests removal 3. Call this endpoint with orderNote="" (empty string) 4. Note cleared from next order Scenario 3: Gift message for specific order 1. Customer's membership ships to recipient monthly 2. Special occasion (birthday) on next delivery 3. Add gift message to next billing attempt only 4. Regular shipments continue without message ``` **Integration Tips:** - Fetch upcoming billing attempts via `/subscription-billing-attempts` endpoint first - Check `status` field to ensure billing attempt can be modified - Display character counter in UI (5000 char limit) - Sanitize input to prevent injection attacks - Consider validating against profanity/spam filters **Error Handling:** - **404**: Billing attempt ID doesn't exist or belongs to different shop - **400**: Billing attempt already processed (status is SUCCESS/FAILED) - **400**: Order note exceeds 5000 character limit - **401**: Invalid or missing API key **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get cancellation flow configuration Source: https://developers.appstle.com/memberships-admin-api/cancellation-flow-configuration/get-cancellation-flow-configuration /memberships/admin-api-swagger.json get /api/external/v2/cancellation-managements/{id} Retrieves the cancellation management settings for the shop, controlling how members cancel memberships. These settings define the cancellation experience including retention offers, surveys, and confirmation flows. **Key Configuration Returned:** - **Cancellation Reasons**: Predefined list of reasons members can select - **Retention Offers**: Discounts or perks offered to prevent cancellations - **Survey Questions**: Custom questions to gather cancellation feedback - **Confirmation Settings**: Text and buttons for cancel confirmation screen - **Minimum Cycle Requirements**: Enforce minimum membership duration before canceling - **Win-Back Campaigns**: Settings for re-engagement after cancellation - **Plan-Specific Rules**: Different cancellation flows per membership plan **Cancellation Flow Elements:** - **Reason Selection**: Dropdown/radio options for why member is canceling - **Feedback Collection**: Open-text fields for additional comments - **Retention Step**: Show special offers before final cancellation - **Alternative Options**: Pause, skip, or frequency change suggestions - **Confirmation Screen**: Final confirmation with cancel button text - **Email Notifications**: Trigger cancellation confirmation emails **Retention Offer Configuration:** - Discount percentage or fixed amount off - Number of cycles the retention discount applies - Eligibility rules (new members only, all members, etc.) - Display message and call-to-action text - Success/failure handling **Use Cases:** - Build custom cancellation flows in member portals - Display retention offers programmatically - Integrate cancellation reasons with analytics platforms - Customize cancel experience per membership tier - Show plan-specific retention messaging - Enforce business rules around cancellations **Common Cancellation Reasons:** - Too expensive - No longer need the product - Delivery issues - Product quality concerns - Switching to competitor - Moving/relocating **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get custom CSS styling configuration Source: https://developers.appstle.com/memberships-admin-api/custom-css-styling/get-custom-css-styling-configuration /memberships/admin-api-swagger.json get /api/external/v2/subscription-custom-csses/{id} Retrieves custom CSS styles configured for membership widgets and customer portal. These styles allow merchants to customize the visual appearance of membership elements to match their brand. **Use Cases:** - Apply custom styling to membership widgets on storefront - Customize customer portal appearance - Match brand colors and fonts - Override default widget styles **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get past discount codes for contract Source: https://developers.appstle.com/memberships-admin-api/customer-discount-history/get-past-discount-codes-for-contract /memberships/admin-api-swagger.json get /api/external/v2/customer-discount-code-infos/get-past-discounts Retrieves historical discount codes that have been applied to or are associated with a specific membership contract. Returns discount details including code, value, usage limits, and expiration dates from Shopify. **Use Cases:** - Display discount history in customer portal - Show applied promotional codes - Track discount redemption for analytics - Verify discount eligibility **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get customer payment token information Source: https://developers.appstle.com/memberships-admin-api/customer-payment-methods/get-customer-payment-token-information /memberships/admin-api-swagger.json get /api/external/v2/customer-payments/token/{customerId} Retrieves payment method token information for a specific customer. This endpoint returns the customer's stored payment tokens used for recurring membership billing. **Key Information Returned:** - **Customer ID**: Shopify customer identifier - **Payment Tokens**: List of stored payment method tokens - **Payment Method Details**: Last 4 digits, card type, expiration - **Default Payment Method**: Primary payment method for memberships - **Token Status**: Active, expired, or failed payment methods - **Payment Gateway**: Stripe, Shopify Payments, etc. **Payment Token Information:** - **Token ID**: Unique identifier for the payment method - **Card Brand**: Visa, Mastercard, Amex, etc. - **Last 4 Digits**: Masked card number ending - **Expiry Date**: Card expiration month and year - **Billing Address**: Associated billing address details - **Created Date**: When payment method was added **Use Cases:** - Display saved payment methods in customer portal - Validate payment method availability before billing - Allow customers to select from existing payment methods - Sync payment data with external CRM or billing systems - Build custom payment management interfaces - Trigger payment update reminders for expiring cards **Security Note:** This endpoint only returns tokenized payment information. Full card numbers are never exposed via the API. **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get customer portal settings Source: https://developers.appstle.com/memberships-admin-api/customer-portal-configuration/get-customer-portal-settings /memberships/admin-api-swagger.json get /api/external/v2/customer-portal-settings/{id} Retrieves customer portal configuration settings for the authenticated shop. These settings control the appearance, behavior, and text labels displayed in the customer self-service portal. **Key Settings Returned:** - **UI Text Labels**: Button text, form field labels, messages, tooltips - **Feature Toggles**: Enable/disable portal features (shipping address edit, pause/resume, etc.) - **Branding**: Custom HTML for header/footer, date formats, localization - **Cancellation Controls**: Minimum days before cancellation, retention discounts - **Address Management**: Shipping address field labels and configurations - **Discount Display**: Discount note titles and descriptions for different cycles - **Product Management**: Add product labels, variant change text, quantity controls - **Order Management**: Order history labels, fulfillment status text - **Pause/Resume**: Pause badge text, resume membership messages - **Rewards Integration**: Rewards text and points display labels **Common Use Cases:** - Retrieve portal settings for display in custom integrations - Sync portal configuration with external systems - Build custom customer portal interfaces using API-driven configuration - Validate current portal settings programmatically - Implement multi-language support by fetching localized labels **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Add product line item to membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/add-product-line-item-to-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contract-add-line-item Adds a new product (line item) to an existing membership contract. This allows customers or merchants to add products to their recurring membership orders. **Key Features:** - **Add Products**: Include new products in future membership deliveries - **Quantity Control**: Specify how many units of the product to add - **Price Override**: Set custom pricing for the added product - **Immediate Effect**: Changes apply to next billing cycle - **Activity Logging**: All additions are tracked in activity logs **Required Parameters:** - **contractId**: Membership contract ID to modify - **variantId**: Shopify product variant ID (with gid:// prefix or numeric) - **quantity**: Number of units to add (must be positive integer) - **price**: Price per unit in shop's base currency **Product Variant ID Format:** Accepts two formats: - Shopify GID: `gid://shopify/ProductVariant/12345678901` - Numeric ID: `12345678901` **Price Behavior:** - Price is per unit, not total - Must be in shop's base currency (USD, EUR, etc.) - Can override product's default price - Does not include taxes or shipping - Total line item cost = price × quantity **Line Item Addition Rules:** - Product variant must exist in Shopify catalog - Product must be active and available - Variant must have sufficient inventory (if tracked) - Contract must be ACTIVE or PAUSED (not CANCELLED) - Cannot add duplicate variants (use update quantity instead) **Common Use Cases:** - Customer adds complementary product to existing membership - Upsell additional products through customer portal - Merchant adds bonus items to customer memberships - Build custom "add-on" product selection interfaces - Cross-sell related products to existing subscribers - Create bundle upgrades (add multiple products at once) **Post-Addition Effects:** - Next billing amount increases by (price × quantity) - Product appears in all future membership deliveries - Customer receives confirmation of membership update - Activity log records the addition with source (portal/merchant) - Contract's next billing date remains unchanged **Integration Example:** ``` PUT /api/external/v2/subscription-contract-add-line-item? contractId=123456& variantId=gid://shopify/ProductVariant/987654321& quantity=2& price=19.99 ``` This adds 2 units of variant 987654321 at $19.99 each ($39.98 total) to contract 123456. **Important Notes:** - Changes apply to NEXT billing cycle, not current/past orders - If product is out of stock, order may be delayed or skipped - Price changes don't affect historical orders - Consider inventory availability before adding products **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Add product to membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/add-product-to-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-add-line-item Adds a new product variant to a membership contract's recurring line items. The product will be included in all future recurring orders. **Product Addition Process:** - **Variant Validation**: Verifies product variant exists and is available - **Add to Contract**: Adds line item to membership's recurring products - **Quantity Setting**: Sets initial quantity for the product - **Price Calculation**: Calculates pricing including any applicable discounts - **Shipping Update**: May recalculate shipping if weight/dimensions change - **Activity Logging**: Records product addition event **Key Features:** - **Recurring Addition**: Product added to every future order - **Quantity Control**: Specify exact quantity to add - **Discount Inheritance**: New item inherits subscription-level discounts - **Immediate Effect**: Applies to next billing cycle - **Freeze Protection**: Validates membership isn't frozen before minimum cycles **Variant ID Format:** - Accepts Shopify variant ID as string - Can be numeric ID or GraphQL format - Example: "12345678" or "gid://shopify/ProductVariant/12345678" **Use Cases:** - Customer upgrades membership to include more products - Cross-sell additional products to existing subscribers - Allow customers to customize memberships in portal - Add seasonal products to memberships - Build-a-box membership customization - Upsell workflows during customer interactions **Important Notes:** - Product must be available for purchase - Membership must not be frozen (before min cycles) - Price updates automatically with membership billing - Quantity must be positive integer **Returns:** Updated membership contract object with new line item **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Apply discount code to membership Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/apply-discount-code-to-membership /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-apply-discount Applies an existing Shopify discount code to a membership contract. The discount code must already exist in Shopify and be valid for membership usage. **Discount Application Process:** - **Code Validation**: Verifies discount code exists in Shopify - **Eligibility Check**: Ensures code is valid for membership use - **Contract Update**: Applies discount to membership contract - **Price Recalculation**: Updates membership pricing with discount applied - **Activity Logging**: Records discount application event **Key Features:** - **Existing Code Support**: Uses Shopify discount codes already created in admin - **Automatic Validation**: Checks code validity and membership eligibility - **Immediate Application**: Discount applies to next billing cycle - **Contract Synchronization**: Keeps Shopify contract in sync with discount **Discount Code Requirements:** - Must exist in Shopify admin - Must be active and not expired - Must be eligible for membership purchases - Usage limits must not be exceeded **Use Cases:** - Allow customers to apply promotional codes to existing memberships - Customer service applying retention discounts - Reward program integration with discount codes - Referral program discount application - Seasonal promotion code application - Win-back campaign discount codes **Returns:** Updated membership contract object with discount applied **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Cancel membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/cancel-membership-contract /memberships/admin-api-swagger.json delete /api/external/v2/subscription-contracts/{id} Cancels a membership contract and sends cancellation confirmation emails. This endpoint terminates the recurring membership and processes all cancellation workflows. **Cancellation Process:** - **Contract Termination**: Marks membership as cancelled in Shopify - **Billing Stop**: Prevents future billing attempts - **Email Notification**: Sends cancellation confirmation to customer - **Activity Logging**: Records cancellation event with source and feedback - **Feedback Capture**: Optional cancellation reason for analytics **Key Features:** - **Immediate Cancellation**: Stops membership processing immediately - **Feedback Collection**: Capture customer cancellation reasons - **Automated Emails**: Customer receives cancellation confirmation - **Activity Tracking**: Logs cancellation to activity history - **Source Attribution**: Tracks whether cancellation came from customer portal or API **Validation Checks:** - **Contract Ownership**: Verifies contract belongs to authenticated shop - **Minimum Cycles**: Checks if minimum billing cycles requirement is met (if configured) - **Freeze Status**: Validates membership is not frozen - **Existing Status**: Ensures contract is not already cancelled **Email Notifications:** - Sends cancellation confirmation to customer email - Includes membership details and cancellation date - Uses customizable email templates from shop settings **Use Cases:** - Allow customers to cancel from custom portals or mobile apps - Bulk cancellation workflows via external systems - Integration with customer service platforms - Automated cancellation based on business rules - Churn management and retention workflows **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Cancel pending downgrade for a membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/cancel-pending-downgrade-for-a-membership-contract /memberships/admin-api-swagger.json delete /api/external/v2/subscription-contract-details/{contractId}/pending-downgrade Cancels a scheduled/pending downgrade for a specific membership contract. This endpoint removes the pending downgrade so that the membership will continue with its current plan. **Important Notes:** - The downgrade must be pending (not yet executed) to be cancelled - Once cancelled, the membership will remain on its current plan - An activity log entry is created when a downgrade is cancelled **Use Cases:** - Allow customers to change their mind about a scheduled downgrade - Cancel downgrades when customers upgrade or renew - Administrative cancellation of pending plan changes **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Create and add custom discount to membership Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/create-and-add-custom-discount-to-membership /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-add-discount Creates a custom discount and applies it to a membership contract. This endpoint allows you to create on-the-fly discounts without requiring pre-existing discount codes in Shopify. **Discount Creation & Application:** - **Custom Discount**: Creates discount directly on membership contract - **Flexible Types**: Supports percentage off or fixed amount off - **Cycle Limits**: Optional limit on number of billing cycles discount applies - **Item-Level Control**: Option to apply discount to each item vs entire order - **Immediate Effect**: Discount applies to next billing cycle **Discount Parameters:** - **Percentage**: Percentage discount (e.g., 15 for 15% off) - use for discountType='PERCENTAGE' - **Amount**: Fixed amount discount (e.g., 10.00 for $10 off) - use for discountType='FIXED_AMOUNT' - **Title**: Display name for the discount (e.g., 'Loyalty Discount') - **Cycle Limit**: Number of billing cycles discount applies (null = unlimited) - **Applies On Each Item**: true = per-item discount, false = order-level discount **Discount Types:** - **PERCENTAGE**: Percentage-based discount (use 'percentage' parameter) - **FIXED_AMOUNT**: Fixed dollar amount discount (use 'amount' parameter) **Cycle Limit Examples:** - `recurringCycleLimit=1`: Discount for first order only - `recurringCycleLimit=3`: Discount for first 3 orders - `recurringCycleLimit=null`: Discount applies forever **Use Cases:** - Customer retention offers (e.g., 20% off next 3 orders) - Loyalty rewards and point redemptions - Win-back campaigns with limited-time discounts - Customer service compensation discounts - Referral program rewards - First-order discounts for new subscribers - Seasonal promotions on existing memberships **Example Scenarios:** 1. **Retention Offer**: 25% off for 2 billing cycles - percentage=25, recurringCycleLimit=2, discountType='PERCENTAGE' 2. **Loyalty Reward**: $5 off every order forever - amount=5.00, recurringCycleLimit=null, discountType='FIXED_AMOUNT' 3. **First Order Deal**: 50% off first order only - percentage=50, recurringCycleLimit=1, discountType='PERCENTAGE' **Returns:** Updated membership contract object with discount applied **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Generate customer portal authentication token Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/generate-customer-portal-authentication-token /memberships/admin-api-swagger.json get /api/external/v2/customer-portal-token Generates an authentication token for customer portal access using either customer ID or email address. This token can be used to create magic links or authenticate API requests on behalf of a customer. **Key Features:** - **Flexible Lookup**: Find customer by Shopify customer ID OR email address - **JWT Token**: Returns cryptographically secure JSON Web Token - **Portal Access**: Token grants access to customer membership management portal - **API Authentication**: Can be used in subsequent API calls for customer-specific operations - **Time-Limited**: Token expires after configured duration (default: 24-72 hours) **Request Parameters:** Provide **either** customerId OR email (not both): - **customerId**: Shopify customer ID (numeric string, e.g., "6789012345") - **email**: Customer's email address as registered in Shopify **Response Contains:** - **customerId**: Shopify customer ID associated with the token - **token**: JWT authentication token for portal access - **shop**: Store domain the customer belongs to - **expiresAt**: Token expiration timestamp (ISO 8601) **Common Use Cases:** - Generate token to construct customer portal magic links - Authenticate customer in headless commerce implementations - Validate customer identity before allowing membership changes - Create custom portal integrations with embedded authentication - Server-side customer lookup when only email is available - Build custom membership management UIs with API authentication - Integrate with external CRM systems requiring customer tokens **Token Usage:** Once generated, the token can be: 1. Embedded in magic link URLs: `https://portal.example.com?token={token}` 2. Used as Bearer token in Authorization headers for API calls 3. Stored temporarily for customer session management 4. Passed to frontend applications for customer-specific operations **Security Best Practices:** - Never expose tokens in client-side logs or browser storage - Transmit tokens only over HTTPS - Implement token rotation for long-lived sessions - Validate token expiration before use - Revoke tokens when customer logs out or changes credentials **Error Handling:** - If neither customerId nor email is provided, returns 400 Bad Request - If both customerId and email are provided, customerId takes precedence - If customer not found, returns 404 Not Found - Invalid email format returns 400 Bad Request **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Generate customer portal magic link Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/generate-customer-portal-magic-link /memberships/admin-api-swagger.json get /api/external/v2/manage-subscription-link/{customerId} Generates a secure, time-limited magic link that allows customers to access and manage their memberships. This passwordless authentication link directs customers to the membership management portal. **Magic Link Features:** - **Passwordless Access**: No login credentials required, link serves as authentication - **Time-Limited**: Link expires after configured duration (typically 24-72 hours) - **Single Customer**: Link is bound to specific customer ID, cannot be reused for others - **Secure Token**: Uses cryptographically secure JWT tokens for authentication - **Customer Portal**: Directs to full-featured self-service membership portal **Returned Information:** - **Magic Link URL**: Full URL to customer portal with embedded authentication token - **Token**: JWT token value (can be used separately if needed) - **Expiration Time**: When the magic link will expire (ISO 8601 format) - **Customer ID**: Shopify customer ID the link is generated for - **Shop Domain**: Store domain where memberships are hosted **Customer Portal Capabilities (via Magic Link):** - View all active and paused membership contracts - Update shipping address for upcoming deliveries - Change payment method for future billing - Pause or resume membership deliveries - Skip upcoming delivery orders - Modify delivery frequency (e.g., monthly to bi-monthly) - Add or remove products from membership - Swap product variants (size, flavor, color) - Cancel membership with reason feedback - View order history and upcoming deliveries - Apply discount codes to membership **Common Use Cases:** - Send magic link via email for customer self-service - Include link in transactional emails (order confirmation, shipping notices) - Customer support: provide link to customers over phone/chat - Embed link in customer account page or dashboard - Automated email campaigns for membership management reminders - Post-purchase flows to encourage membership modifications - Win-back campaigns: send magic link to cancelled/paused customers **Security Notes:** - Links are single-use per session (new token generated each time) - Tokens include shop and customer validation to prevent tampering - Expired links automatically redirect to token request page - Links should be sent via secure channels (HTTPS, encrypted email) **Integration Best Practices:** - Always send magic links via email or SMS (don't display on public pages) - Set appropriate expiration time based on use case - Include clear call-to-action in emails ("Manage Your Membership") - Handle expired tokens gracefully with re-send functionality - Track magic link generation for security audit logs **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get billing interval options for selling plans Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-billing-interval-options-for-selling-plans /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-details/billing-interval Retrieves available billing frequency/interval options for specified Shopify selling plans (membership plans). This endpoint returns all configured billing frequencies that customers can choose from for their memberships. **Key Information Returned:** - **Frequency Options**: List of available billing intervals (e.g., weekly, monthly, quarterly) - **Interval Units**: Time unit for each option (DAYS, WEEKS, MONTHS, YEARS) - **Interval Count**: Number of units between billings (e.g., 2 = every 2 weeks) - **Display Names**: Customer-friendly names for each frequency option - **Plan IDs**: Associated selling plan IDs for each frequency **Billing Frequency Structure:** Each FrequencyInfoDTO contains: - **id**: Selling plan ID (Shopify membership plan identifier) - **name**: Display name (e.g., "Deliver every 2 weeks", "Monthly membership") - **intervalUnit**: Time unit (DAY, WEEK, MONTH, YEAR) - **intervalCount**: Number of units between deliveries - **billingPolicy**: How billing is configured (EXACT_DAY, ANNIVERSARY) - **deliveryPolicy**: How delivery is scheduled **Common Billing Intervals:** - **Weekly**: intervalUnit=WEEK, intervalCount=1 - **Bi-Weekly**: intervalUnit=WEEK, intervalCount=2 - **Monthly**: intervalUnit=MONTH, intervalCount=1 - **Every 2 Months**: intervalUnit=MONTH, intervalCount=2 - **Quarterly**: intervalUnit=MONTH, intervalCount=3 - **Semi-Annual**: intervalUnit=MONTH, intervalCount=6 - **Annual**: intervalUnit=YEAR, intervalCount=1 **Request Parameters:** - **sellingPlanIds**: Comma-separated list of Shopify selling plan IDs (required) - Example: "123456,123457,123458" - Returns frequency options for all specified plans **Common Use Cases:** - Display available billing frequencies in customer portal - Allow customers to change membership delivery frequency - Build frequency selector UI components - Validate frequency options before updating membership - Sync available billing options with external systems - Show frequency options during membership checkout **Integration Example:** 1. Get customer's current membership selling plan ID 2. Call this endpoint with that selling plan ID 3. Display returned frequency options to customer 4. Customer selects new frequency 5. Update membership with selected frequency **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get customer payment methods Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-customer-payment-methods /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-details/shopify/customer/{customerId}/payment-methods Retrieves available Shopify payment methods for a specific customer from the customer portal. # Get detailed customer membership information Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-detailed-customer-membership-information /memberships/admin-api-swagger.json get /api/external/v2/subscription-customers-detail/valid/{customerId} Retrieves comprehensive details for all membership contracts associated with a specific customer. This endpoint returns complete membership contract data including products, pricing, billing schedule, and status. **Key Information Returned:** - **Contract Details**: Contract ID, status (active, paused, cancelled), creation date - **Products**: Line items with product names, variants, quantities, and prices - **Billing Information**: Next billing date, billing frequency, payment method - **Delivery Details**: Shipping address, delivery method, delivery frequency - **Pricing**: Subtotal, discounts applied, total amount per cycle - **Membership Plan**: Associated membership plan name and details - **Order History**: Past billing attempts and fulfillment records **Membership Contract Details Include:** - **Contract ID**: Unique identifier for the membership - **Customer ID**: Shopify customer ID associated with the contract - **Status**: ACTIVE, PAUSED, CANCELLED, EXPIRED - **Billing Cycle**: Number of completed billing cycles - **Next Billing Date**: When the next payment will be charged - **Delivery Date**: When the next order will be shipped - **Products**: All line items in the membership with pricing - **Discount Codes**: Applied discount codes and their values - **Custom Attributes**: Any custom metadata or tags **Common Use Cases:** - Display full membership list in customer account dashboard - Retrieve all contract details for customer support inquiries - Build custom customer portal with membership management - Sync membership data to external CRM or analytics systems - Generate customer membership reports and analytics - Validate customer's active memberships before offering upgrades - Show membership history and upcoming deliveries **Response Format:** Returns a List of SubscriptionContractDetailsDTO objects, one per active/paused contract. If customer has no memberships, returns an empty list. **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get membership contract list Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-membership-contract-list /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-details Retrieves a paginated list of membership contracts with advanced filtering capabilities. This endpoint provides comprehensive access to all membership contracts in your store with flexible query options. **Key Information Returned:** - **Contract Details**: Contract ID, status, creation date, billing cycle - **Customer Information**: Customer name, email, Shopify customer ID - **Membership Items**: Products, variants, quantities in membership - **Billing Information**: Next billing date, billing frequency, pricing - **Delivery Details**: Shipping address, delivery method, next delivery date - **Plan Information**: Membership plan type, selling plan IDs - **Status Tracking**: Active, paused, cancelled, expired states **Filtering Capabilities:** - **Date Ranges**: Filter by creation date or next billing date - **Customer Search**: Search by customer name or email - **Status Filter**: Filter by contract status (active, paused, cancelled) - **Plan Type**: Filter by membership plan type - **Billing Frequency**: Filter by billing interval (weekly, monthly, etc.) - **Product/Variant**: Find contracts containing specific products or variants - **Order Name**: Search by Shopify order name/number - **Selling Plan**: Filter by Shopify selling plan IDs - **Special Filters**: Contracts with deleted products or bounced emails **Pagination:** - Supports standard pagination parameters (page, size, sort) - Returns pagination headers for total count and navigation - Default page size configurable via request parameters **Use Cases:** - Export membership data to external CRM or analytics systems - Build custom membership dashboards and reports - Sync membership data with external platforms - Identify memberships needing attention (bounced emails, deleted products) - Generate membership activity reports by date range - Find all memberships for a specific product or variant - Monitor membership health and churn metrics **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get membership customer details Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-membership-customer-details /memberships/admin-api-swagger.json get /api/external/v2/subscription-customers/{customerId} Retrieves comprehensive customer information from Shopify for a specific customer ID. This endpoint fetches the customer record directly from Shopify's API, returning all personal details, addresses, payment methods, and metadata associated with the customer account. **What This Endpoint Returns:** Provides a complete Shopify Customer object containing all customer-related data for building customer portals, validating identities, displaying profiles, or integrating with external CRM/analytics systems. **Response Data Structure:** ```json { "id": "gid://shopify/Customer/6789012345", "email": "customer@example.com", "firstName": "Jane", "lastName": "Smith", "phone": "+1-555-123-4567", "displayName": "Jane Smith", "defaultAddress": { "address1": "123 Main St", "city": "San Francisco", "province": "CA", "zip": "94102", "country": "United States" }, "addresses": [...], "tags": ["VIP", "Subscriber"], "note": "Prefers morning deliveries", "state": "ENABLED", "createdAt": "2023-01-15T10:30:00Z" } ``` **Customer Data Fields Returned:** **Personal Information:** - `id` - Shopify global customer ID (format: `gid://shopify/Customer/{numeric_id}`) - `firstName` - Customer's first name - `lastName` - Customer's last name - `displayName` - Full name for display purposes - `email` - Primary email address (unique identifier) - `phone` - Contact phone number (optional, may be null) **Address Information:** - `defaultAddress` - Primary shipping/billing address object - `address1`, `address2` - Street address lines - `city`, `province`, `zip`, `country` - Location details - `provinceCode`, `countryCodeV2` - Standardized codes - `company` - Company name (if B2B customer) - `addresses` - Array of all saved addresses (shipping + billing) **Account Status:** - `state` - Account status: `ENABLED`, `DISABLED`, `INVITED`, `DECLINED` - `verifiedEmail` - Whether email has been verified (boolean) - `taxExempt` - Tax exemption status (boolean) - `acceptsMarketing` - Email marketing opt-in status **Metadata & Custom Fields:** - `tags` - Array of customer tags for segmentation - `note` - Merchant notes about the customer - `metafields` - Custom data fields (if queried) - `numberOfOrders` - Total lifetime order count **Timestamps:** - `createdAt` - When customer account was created - `updatedAt` - Last modification timestamp **Common Use Cases & Integration Scenarios:** **1. Customer Portal - Profile Display** ``` Use Case: Show customer their account information 1. Get customerId from session/JWT token 2. Call GET /subscription-customers/{customerId} 3. Display: Name, Email, Default Address 4. Show "Edit Profile" button linking to address update endpoint ``` **2. Pre-fill Shipping Address Form** ``` Use Case: Auto-populate address form when updating shipping 1. Fetch customer data via this endpoint 2. Extract defaultAddress object 3. Pre-fill form fields with existing address 4. Allow customer to edit and submit changes ``` **3. Identity Verification Before Critical Actions** ``` Use Case: Verify customer email before canceling membership 1. User clicks "Cancel Membership" 2. Fetch customer data 3. Compare session email with customer.email 4. If mismatch: Reject (security violation) 5. If match: Proceed with cancellation ``` **4. CRM Integration / Analytics** ``` Use Case: Sync customer data to external CRM (Salesforce, HubSpot) 1. Webhook triggers on customer update 2. Call this endpoint to get fresh customer data 3. Map fields to CRM schema 4. Push to CRM via their API ``` **5. Customer Segmentation** ``` Use Case: Filter customers by tags for targeted campaigns 1. Fetch customer details 2. Check tags array for "VIP" or "Churned" 3. Apply special pricing or retention offers 4. Customize email communications ``` **Customer ID Format & Validation:** - **Input**: Numeric Shopify customer ID (e.g., `6789012345`) - **Not Accepted**: GraphQL global ID format (will cause 400 error) - **Extraction from GraphQL ID**: If you have `gid://shopify/Customer/6789012345`, extract `6789012345` - **Example Valid IDs**: `123`, `456789`, `9876543210` **Privacy & Security Considerations:** **PII Protection:** - This endpoint returns **personally identifiable information (PII)** - Ensure compliance with GDPR, CCPA, and privacy regulations - Only expose customer data to authenticated, authorized users - Do not log full customer records (contains emails, addresses, phone numbers) **Access Control:** - Customer can only access their own data (enforced by authentication) - Merchants can access all customers via API key - Never expose API keys in frontend code - Use server-to-server calls for merchant access **Data Minimization:** - Only fetch customer data when necessary - Cache responsibly with short TTL (5-10 minutes max) - Clear cache on customer updates **Error Handling & Edge Cases:** **404 - Customer Not Found:** ``` Reasons: - Customer ID doesn't exist in Shopify - Customer was deleted from Shopify admin - Wrong shop (customer belongs to different store) - Typo in customer ID Solution: - Verify customer ID is correct - Check if customer exists in Shopify admin - Ensure shop domain matches customer's store ``` **400 - Invalid Customer ID Format:** ``` Reasons: - Non-numeric customer ID provided - Negative number or zero - GraphQL format instead of numeric ID Solution: - Ensure customer ID is positive integer - Extract numeric portion from GraphQL ID if needed ``` **401 - Authentication Failed:** ``` Reasons: - Missing API key or customer portal token - Expired authentication token - Invalid API key for the shop Solution: - Verify X-API-Key header is set - Regenerate API key if compromised - Check token expiration ``` **Null/Empty Fields:** Some fields may be null/empty if customer hasn't provided data: - `phone` - Not all customers provide phone numbers - `addresses` - New customers may have empty address list - `defaultAddress` - Could be null if no addresses saved - `note` - Empty unless merchant added notes - `tags` - Empty array if no tags assigned Always handle null checks in your code. **Performance & Caching Recommendations:** - **Response Time**: Typically 100-300ms (Shopify GraphQL query) - **Rate Limits**: Subject to Shopify API rate limits (40 requests/second) - **Caching**: Safe to cache for 5-10 minutes (customer data changes infrequently) - **Optimization**: Fetch once per session, store in memory/local state **Related Endpoints:** - `/subscription-customers-detail/valid/{customerId}` - Gets customer + membership details - `/subscription-contract-details` - Lists all memberships for customer - `/customer-payments/token/{customerId}` - Get customer payment tokens - `/customer-portal-token` - Generate authentication token for customer **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter. Customer portal tokens also supported for self-service access. # Get membership order fulfillment details Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-membership-order-fulfillment-details /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-details/subscription-fulfillments/{contractId} Retrieves fulfillment information for the most recent order generated by a membership contract. Shows shipping status, tracking numbers, and delivery progress. **Key Features:** - **Latest Order**: Returns fulfillment data for the most recent membership order - **Tracking Info**: Includes tracking numbers, URLs, and shipping carriers - **Fulfillment Status**: Shows whether items are unfulfilled, fulfilled, or partially fulfilled - **Multi-Fulfillment**: Handles orders split across multiple shipments **Returned Fulfillment Data:** - **Order Info**: Order name/number, creation date, financial status - **Fulfillment Status**: FULFILLED, PARTIAL, UNFULFILLED, etc. - **Fulfillments List**: Array of all fulfillments for the order - **Tracking Numbers**: Tracking codes for each fulfillment - **Tracking URLs**: Direct links to carrier tracking pages - **Shipping Carrier**: Carrier name (USPS, FedEx, UPS, etc.) - **Fulfillment Date**: When each shipment was fulfilled - **Line Items**: Which products/variants are in each shipment - **Delivery Address**: Where the order is being shipped **Common Use Cases:** - **Order Tracking**: Show customers where their membership delivery is - **Customer Portal**: Display "Track Your Order" links - **Shipping Updates**: Check fulfillment status for recent orders - **Support**: Help customers track delayed or missing shipments - **Notifications**: Trigger custom shipping notifications - **Analytics**: Track fulfillment performance metrics **Response Structure:** Returns an Order object containing: - Basic order information - Array of fulfillments with tracking details - Line items per fulfillment - Shipping and delivery information **Note:** This returns data for the MOST RECENT order only. For historical fulfillment data, use order history endpoints. **Parameters:** - **contractId** (required, path): The membership contract ID **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get pending downgrade for a membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-pending-downgrade-for-a-membership-contract /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-details/{contractId}/pending-downgrade Retrieves the pending/scheduled downgrade details for a specific membership contract. This endpoint returns information about a downgrade that has been scheduled but not yet executed. **Response Information:** - **contractId**: The membership contract ID - **waitTillTimestamp**: When the downgrade will be executed - **oldVariantId**: The current product variant ID - **newVariantId**: The target product variant ID after downgrade - **oldPrice**: Current membership price - **newPrice**: New price after downgrade - **sellingPlanId**: The selling plan ID for the new membership - **sellingPlanName**: Name of the selling plan **Use Cases:** - Check if a customer has a pending downgrade scheduled - Display pending downgrade information in custom dashboards - Verify downgrade details before allowing further modifications **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get raw membership contract details from Shopify Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-raw-membership-contract-details-from-shopify /memberships/admin-api-swagger.json get /api/external/v2/subscription-contracts/contract-external/{contractId} Retrieves the complete membership contract data directly from Shopify's GraphQL API. This returns the full, unmodified Shopify contract object with all nested data. **Key Features:** - **Raw Shopify Data**: Direct response from Shopify API, not transformed - **Complete Contract**: All contract fields, line items, billing details - **Real-Time Data**: Fetches current state from Shopify (not cached) - **Nested Objects**: Includes customer, addresses, discounts, line items **Returned Contract Data:** - **Contract ID & Status**: Shopify global ID, status (ACTIVE/PAUSED/CANCELLED/EXPIRED) - **Customer Info**: Customer object with name, email, addresses - **Line Items**: Products/variants in membership with quantities, prices - **Billing Details**: Next billing date, billing policy (frequency, interval) - **Delivery Details**: Delivery policy, shipping address, method - **Pricing**: Line prices, discounts, currency code - **Dates**: Created at, updated at, next billing date - **Custom Attributes**: Any custom data attached to the contract - **Discounts**: Applied discount codes with amounts **Common Use Cases:** - **Full Contract Display**: Show all contract details in admin/portal - **Debugging**: Inspect raw Shopify contract structure - **Data Export**: Get complete contract for reporting/analytics - **Integration Development**: Understand Shopify's contract schema - **Audit Trail**: Capture complete contract state at a point in time **Differences from other endpoints:** - This returns Shopify's raw contract object - `/api/external/v2/subscription-contract-details` returns transformed DTO - This includes more nested Shopify-specific fields **Parameters:** - **contractId** (required, path): The membership contract ID (numeric) **Response:** Returns complete Shopify SubscriptionContract GraphQL object **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get valid membership contract IDs for customer Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/get-valid-membership-contract-ids-for-customer /memberships/admin-api-swagger.json get /api/external/v2/subscription-customers/valid/{customerId} Retrieves a list of all valid (active, paused, or pending) membership contract IDs associated with a specific customer. This endpoint is useful for quickly checking which memberships a customer has without retrieving full contract details. **Key Features:** - **Quick Lookup**: Returns only contract IDs, not full contract details - **Active Memberships Only**: Excludes cancelled or expired contracts - **Set Response**: Returns unique contract IDs (no duplicates) - **Fast Performance**: Lightweight query for list views **Included Membership Statuses:** - **ACTIVE**: Currently active recurring memberships - **PAUSED**: Temporarily paused but valid memberships - **PENDING**: Scheduled to start in the future **Excluded Membership Statuses:** - **CANCELLED**: Customer-cancelled memberships - **EXPIRED**: Reached max cycles or end date - **FAILED**: Failed billing with no recovery **Common Use Cases:** - **Membership Count**: Quickly determine how many active memberships a customer has - **Access Control**: Verify customer has valid memberships before showing portal - **List Navigation**: Build dropdown or list of customer's memberships - **Bulk Operations**: Get all contract IDs for batch processing - **Validation**: Check if customer has any active memberships - **Dashboard Display**: Show membership count without full data **Example Response:** ```json [12345, 12346, 12389] ``` **Parameters:** - **customerId** (required, path): The Shopify customer ID **Response:** Returns a Set of Long values representing valid membership contract IDs **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Remove discount from membership Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/remove-discount-from-membership /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-remove-discount Removes a discount from a membership contract. This endpoint allows you to remove previously applied discounts, restoring the membership to full price. **Discount Removal Process:** - **Identify Discount**: Uses discount ID to locate specific discount on contract - **Remove From Contract**: Removes discount allocation from membership - **Recalculate Pricing**: Updates membership pricing to remove discount - **Activity Logging**: Records discount removal event - **Immediate Effect**: Changes apply to next billing cycle **Key Features:** - **Selective Removal**: Remove specific discounts by ID - **Price Restoration**: Returns membership to original or remaining discount pricing - **Multiple Discount Support**: Works with memberships having multiple discounts - **Activity Tracking**: Logs removal for audit trail **Finding Discount ID:** - Retrieve membership contract details to see applied discounts - Each discount has a unique Shopify GraphQL ID - Format: `gid://shopify/SubscriptionManualDiscount/[ID]` **Use Cases:** - End limited-time promotional discounts - Remove expired retention offers - Customer service discount adjustments - Clean up incorrectly applied discounts - Remove trial period pricing - End referral program bonuses **Returns:** Updated membership contract object with discount removed **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Remove product from membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/remove-product-from-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-remove-line-item Removes a line item (product) from a membership contract. The product will no longer be included in future recurring orders. **Product Removal Process:** - **Line Item Identification**: Locates specific line item using GraphQL line ID - **Contract Update**: Removes line item from membership - **Discount Handling**: Optionally removes associated line-item discounts - **Price Recalculation**: Updates membership total after removal - **Activity Logging**: Records product removal event **Key Features:** - **Selective Removal**: Remove specific products by line ID - **Discount Control**: Choose whether to remove product-specific discounts - **Immediate Effect**: Changes apply to next billing cycle - **Price Updates**: Automatically adjusts membership pricing **Discount Removal Option:** - **removeDiscount=true** (default): Removes discounts tied to this line item - **removeDiscount=false**: Keeps discounts (may apply to other items if applicable) - Only affects line-item-specific discounts, not subscription-level discounts **Line ID Format:** - GraphQL ID format: `gid://shopify/SubscriptionLine/[ID]` - Retrieve from membership contract details endpoint - Each line item has unique ID **Use Cases:** - Customer downgrades membership to fewer products - Remove seasonal products at end of season - Customer portal product removal workflows - Discontinued product cleanup - Membership simplification (too many items) - Build-a-box customization changes **Important Notes:** - Cannot remove last line item (membership needs at least one product) - Membership must not be frozen - Removal is permanent (re-add if needed) - May affect shipping costs if weight/volume changes **Returns:** Updated membership contract object with line item removed **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Send customer portal magic link via email Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/send-customer-portal-magic-link-via-email /memberships/admin-api-swagger.json get /api/external/v2/subscription-contracts-email-magic-link Automatically generates and emails a secure magic link to the customer for accessing their membership management portal. This is a convenience endpoint that combines token generation and email delivery in a single API call. **Key Features:** - **Automated Delivery**: Generates magic link and sends email in one operation - **Customer Lookup**: Finds customer by email address automatically - **Branded Emails**: Uses shop's configured email templates and branding - **Membership Validation**: Only sends if customer has active memberships - **Time-Limited Links**: Email contains token that expires after configured duration **Email Contents:** The sent email typically includes: - **Magic Link Button**: One-click access to customer portal - **Link URL**: Full URL with embedded authentication token - **Expiration Notice**: When the link will expire (e.g., "Valid for 24 hours") - **Shop Branding**: Store logo, colors, and custom messaging - **Help Text**: Instructions on how to use the portal **Workflow:** 1. API receives customer email address 2. System looks up customer in Shopify by email 3. Validates customer has active membership contracts 4. Generates secure JWT authentication token 5. Constructs magic link URL with token 6. Sends branded email with magic link to customer 7. Returns success confirmation to API caller **Email Template Customization:** Email appearance and content can be customized via: - Shop's email notification settings in admin - Custom email templates for magic links - Localization settings for customer's language - Custom branding (logo, colors, footer) **Common Use Cases:** - Customer support: send portal access link to customers over phone/chat - Automated workflows: trigger magic link emails based on events - Forgot password alternative: passwordless portal access - Post-purchase flows: send portal access after first order - Reactivation campaigns: re-engage paused/cancelled customers - Customer onboarding: welcome emails with portal access - Membership reminders: include portal link in reminder emails **Integration Example:** ``` GET /api/external/v2/subscription-contracts-email-magic-link? email=customer@example.com& api_key=your_api_key ``` This sends a magic link email to customer@example.com for portal access. **Error Handling:** - If customer not found, returns 400 Bad Request - If customer has no active memberships, returns 400 - If email service fails, returns 500 Internal Server Error - Invalid email format returns 400 Bad Request **Email Delivery Notes:** - Emails are sent asynchronously (may take 1-5 minutes to deliver) - Check spam folders if customer doesn't receive email - Respect customer's email preferences and unsubscribe status - Rate limits may apply to prevent abuse **Security Considerations:** - Only send to verified customer email addresses - Email contains passwordless authentication link - Links expire after configured duration (default: 24-72 hours) - Sending to wrong email grants that person membership access - Consider email verification before using this endpoint **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Sync customer information Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/sync-customer-information /memberships/admin-api-swagger.json delete /api/external/v2/subscription-customers/sync-info/{customerId} Triggers synchronization of customer information from Shopify for the specified customer ID. # Update billing interval for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-billing-interval-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-billing-interval Changes the billing frequency of a membership contract. This allows customers to modify how often they are charged for their membership. **Key Features:** - **Flexible Billing**: Change billing frequency (daily, weekly, monthly, yearly) - **Custom Intervals**: Set custom interval counts (e.g., every 2 weeks, every 3 months) - **Immediate Effect**: Changes apply to the next billing cycle - **Independent from Delivery**: Billing interval can differ from delivery interval - **Activity Logging**: All changes are logged for audit trail **Billing Interval Options:** - **DAY**: Daily billing (intervalCount: how many days) - **WEEK**: Weekly billing (intervalCount: how many weeks) - **MONTH**: Monthly billing (intervalCount: how many months) - **YEAR**: Yearly billing (intervalCount: how many years) **Common Examples:** - `intervalCount=1, interval=MONTH`: Bill monthly - `intervalCount=2, interval=WEEK`: Bill every 2 weeks - `intervalCount=3, interval=MONTH`: Bill quarterly - `intervalCount=6, interval=MONTH`: Bill semi-annually - `intervalCount=1, interval=YEAR`: Bill annually **Common Use Cases:** - Customer wants to change from monthly to quarterly billing - Switch from annual to monthly payments - Adjust billing frequency to match cash flow - Promotional frequency changes - Align billing with payday schedules **Important Notes:** - **Next Billing Date**: System recalculates the next billing date based on new interval - **Pro-rata Billing**: No pro-rata adjustment; new interval starts from next billing date - **Minimum Cycles**: Respects minimum billing cycle requirements if configured - **Customer Portal Protection**: Includes freeze checks if called from customer portal **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update custom attributes on membership line item Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-custom-attributes-on-membership-line-item /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-attributes Updates custom attributes (metadata) on a specific membership line item. Attributes allow storing custom key-value data on individual products in a membership. **Attribute Update Process:** - **Line Item Identification**: Locates specific line item using GraphQL line ID - **Attribute Replacement**: Replaces existing attributes with new list - **Contract Update**: Syncs changes to Shopify membership contract - **Activity Logging**: Records attribute modification event - **Freeze Validation**: Ensures membership isn't frozen before updates **Custom Attributes:** - **Key-Value Pairs**: Store arbitrary metadata on line items - **Flexible Data**: Supports text, numbers, JSON as values - **Per-Item Storage**: Each line item has independent attributes - **Order Propagation**: Attributes carry forward to generated orders **Attribute Structure:** ```json [ {"key": "gift_message", "value": "Happy Birthday!"}, {"key": "custom_option", "value": "medium_roast"}, {"key": "special_instructions", "value": "Leave at door"} ] ``` **Line ID Format:** - GraphQL ID format: `gid://shopify/SubscriptionLine/[ID]` - Retrieve from membership contract details endpoint - Each line item has unique ID **Use Cases:** - Store gift message customizations per product - Track product-specific preferences (roast level, flavor, size) - Custom delivery instructions for specific items - Personalization options (monogram, engraving text) - Build-a-box selection metadata - Product customization workflows - Integration data synchronization **Important Notes:** - Attributes are replaced, not merged (send all desired attributes) - Maximum 250 characters per value - Membership must not be frozen - Attributes visible in Shopify admin and order details **Returns:** Updated membership contract object with modified attributes **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update custom note attributes on membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-custom-note-attributes-on-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-custom-note-attributes Updates custom note attributes (key-value pairs) on a membership contract. These attributes are stored on the Shopify membership contract and propagated to recurring orders. **Modes:** - `overwriteExistingAttributes=true` (default): Replaces all existing note attributes with the provided list - `overwriteExistingAttributes=false`: Merges provided attributes with existing ones (updates matching keys, adds new ones) **Use Cases:** - Clear tracking pixel IDs from recurring orders - Update order-level metadata - Store custom order instructions **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update delivery interval for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-delivery-interval-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-delivery-interval Changes the delivery/shipping frequency of a membership contract. This allows customers to modify how often they receive their membership orders. **Key Features:** - **Flexible Delivery**: Change delivery frequency (daily, weekly, monthly, yearly) - **Custom Intervals**: Set custom interval counts (e.g., every 2 weeks, every 3 months) - **Independent from Billing**: Delivery interval can differ from billing interval - **Immediate Effect**: Changes apply to the next delivery cycle - **Activity Logging**: All changes are logged for audit trail **Delivery Interval Options:** - **DAY**: Daily delivery (intervalCount: how many days) - **WEEK**: Weekly delivery (intervalCount: how many weeks) - **MONTH**: Monthly delivery (intervalCount: how many months) - **YEAR**: Yearly delivery (intervalCount: how many years) **Common Examples:** - `intervalCount=1, interval=MONTH`: Deliver monthly - `intervalCount=2, interval=WEEK`: Deliver every 2 weeks - `intervalCount=3, interval=MONTH`: Deliver quarterly - `intervalCount=1, interval=WEEK`: Deliver weekly - `intervalCount=6, interval=MONTH`: Deliver twice a year **Billing vs. Delivery Intervals:** These can be different! For example: - **Bill monthly, deliver weekly**: Customer pays monthly but receives weekly shipments - **Bill quarterly, deliver monthly**: Customer pays every 3 months but receives monthly shipments - **Bill annually, deliver monthly**: Customer pays yearly upfront for monthly deliveries **Common Use Cases:** - Customer wants deliveries less frequently (save on shipping) - Customer wants deliveries more frequently (use products faster) - Adjust delivery to match consumption rate - Seasonal frequency changes (more in summer, less in winter) - Align deliveries with schedule (delivery when home from vacation) **Important Notes:** - **Billing Date Unchanged**: Only affects delivery schedule, not billing schedule - **Next Delivery Date**: System recalculates next delivery date based on new interval - **Order Fulfillment**: Each delivery creates a new order at the specified interval - **Prepaid Memberships**: Particularly useful for prepaid plans with multiple deliveries **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update maximum billing cycles for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-maximum-billing-cycles-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-max-cycles Sets the maximum number of billing cycles (payments) after which the membership will automatically expire and cancel. This is useful for creating fixed-term memberships or limited-duration memberships. **Key Features:** - **Auto-Expiration**: Membership automatically cancels after max cycles reached - **Fixed-Term Plans**: Create memberships with defined end dates - **No Manual Intervention**: System handles cancellation automatically - **Flexible Duration**: Set any number or null for unlimited **Common Use Cases:** - **6-Month Program**: Set maxCycles=6 for half-year membership - **Annual Membership**: Set maxCycles=12 for one-year auto-expiring plan - **Trial Extensions**: Set maxCycles=3 for limited trial periods - **Unlimited Membership**: Set maxCycles=null or 0 for no maximum **Behavior:** - Membership cancels automatically after final billing cycle - Customer receives notification before expiration - No refunds issued on auto-cancellation - Contract status changes to EXPIRED after last cycle **Parameters:** - **contractId** (required): The membership contract ID - **maxCycles** (optional): Maximum number of billing cycles (null or 0 = unlimited) **Note:** Setting maxCycles lower than current cycle count will cause immediate expiration. **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update membership contract billing date Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-membership-contract-billing-date /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-billing-date Updates the next billing date for a membership contract. This reschedules when the next payment will be charged for the membership. **Key Features:** - **Flexible Rescheduling**: Move billing date forward or backward - **Immediate Effect**: Changes take effect immediately in the billing schedule - **Automatic Adjustments**: Future billing dates recalculate based on frequency - **Customer Portal Compatible**: Can be triggered from customer self-service portal - **Activity Logging**: All changes are tracked in activity logs **Billing Date Update Rules:** - New date must be in the future (past dates are rejected) - Cannot update if contract is paused or cancelled - Cannot update if minimum billing cycles enforcement is active - Date format must be ISO 8601 with timezone (e.g., 2024-12-25T10:00:00Z) - Subsequent billing dates auto-calculate based on delivery frequency **Common Use Cases:** - Customer requests to postpone next delivery/billing - Align billing date with customer payday or preference - Skip a billing cycle due to vacation or temporary hold - Synchronize multiple memberships to bill on same day - Resolve payment timing conflicts or scheduling issues - Adjust for seasonal demand or customer availability **Important Notes:** - Changing billing date does NOT change the billing cycle frequency - If minimum cycles are configured, this may be restricted - Activity logs capture old date, new date, and change source - Customer receives updated billing schedule notification **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update membership contract status (pause/resume/activate) Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-membership-contract-status-pauseresumeactivate /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-status Changes the status of a membership contract. This allows pausing, resuming, or activating memberships to control billing and delivery. **Key Features:** - **Pause Memberships**: Temporarily stop billing and deliveries - **Resume Memberships**: Reactivate paused memberships - **Activate Memberships**: Start inactive memberships - **Activity Logging**: All status changes are logged for audit trail - **Customer Restrictions**: Includes minimum cycle checks when called from customer portal **Status Values:** - **ACTIVE**: Membership is active and billing/delivering normally - **PAUSED**: Membership is temporarily paused (no billing, no deliveries) - **CANCELLED**: Membership is cancelled (use DELETE endpoint instead) **Pause vs. Cancel:** - **Pause**: Temporary hold, customer can resume anytime - **Cancel**: Permanent termination, requires creating new membership to restart **Common Use Cases:** - **Vacation Hold**: Customer going on vacation, pause deliveries temporarily - **Financial Pause**: Customer needs temporary break from payments - **Product Surplus**: Customer has too much product, pause until they use it - **Seasonal Pause**: Pause memberships during off-season (e.g., lawn care in winter) - **Resume After Pause**: Customer ready to restart after temporary hold - **Reactivate Failed**: Reactivate membership after payment method updated **Pause Behavior:** - **Billing Paused**: No charges while paused - **Deliveries Paused**: No orders created while paused - **Next Billing Date**: Preserved or recalculated on resume (merchant setting) - **Unlimited Duration**: Pauses can be indefinite unless merchant sets limits **Resume Behavior:** - **Immediate Reactivation**: Membership becomes active immediately - **Next Billing Date**: Calculated based on pause duration settings - **Deliveries Resume**: Next delivery scheduled according to interval **Customer Portal Restrictions:** When called from customer portal (vs. merchant API): - **Minimum Cycles**: Cannot pause/cancel until minimum billing cycles met - **Freeze Period**: Updates may be frozen until minimum requirements satisfied - **Retention Rules**: Special retention discounts may be offered on cancellation **Important Notes:** - **Customer Communication**: Consider sending email notifications on status changes - **Billing Cycles Not Lost**: Paused cycles don't count toward minimums - **No Pro-rata**: No refunds or credits when pausing mid-cycle **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update membership line item quantity and pricing Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-membership-line-item-quantity-and-pricing /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item Updates an existing line item in a membership contract, modifying quantity, variant, or price. This endpoint allows comprehensive updates to membership products. **Line Item Update Process:** - **Line Item Identification**: Locates line item using GraphQL line ID - **Quantity Update**: Changes product quantity for recurring orders - **Variant Change**: Can switch to different variant of same product - **Price Override**: Optional custom pricing override - **Contract Sync**: Updates Shopify membership contract - **Activity Logging**: Records line item modification event **Update Capabilities:** - **Quantity Adjustment**: Increase or decrease product quantity - **Variant Switching**: Change product variant (size, color, flavor, etc.) - **Custom Pricing**: Override standard pricing per line item - **Combined Updates**: Change multiple properties in single request **Parameters:** - **lineId**: GraphQL ID of line item to update - **quantity**: New quantity (must be positive integer) - **variantId**: Product variant ID (can change to different variant) - **price**: Custom price override (optional, overrides default pricing) **Price Override:** - Allows custom per-line pricing - Useful for special pricing arrangements - Overrides default product pricing - Price in shop's base currency **Use Cases:** - Customer increases/decreases product quantity - Switch product variant (e.g., Medium → Large roast) - Special pricing for VIP customers - Seasonal variant changes (Summer → Winter flavor) - Customer portal quantity adjustments - Wholesale pricing overrides - Build-a-box quantity modifications **Important Notes:** - Quantity must be positive (use remove-line-item to delete) - Variant must be valid and available - Price updates don't affect existing discount percentages - Changes apply to next billing cycle **Returns:** Updated membership contract object with modified line item **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update membership payment method Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-membership-payment-method /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-payment-method Updates the payment method for a membership contract by refreshing payment instrument from Shopify customer. This endpoint syncs the membership's payment method with the customer's default payment method in Shopify. **Payment Method Update Process:** - **Fetch Latest Payment**: Retrieves customer's current default payment method from Shopify - **Update Contract**: Associates new payment method with membership contract - **Validate Payment**: Ensures payment method is valid and active - **Sync Changes**: Updates payment instrument in membership billing system **Key Features:** - **Automatic Sync**: Pulls latest payment method from Shopify customer record - **Payment Validation**: Verifies new payment method is usable for billing - **Contract Update**: Updates Shopify membership contract with new payment - **Failed Billing Recovery**: Useful for updating payment after billing failures **Use Cases:** - Customer updates credit card and wants to apply to existing membership - Recover from failed billing by allowing payment method update - Sync payment methods in customer portal workflows - Update expired or invalid payment methods - Switch between multiple saved payment methods - Integration with custom payment update flows **Important Notes:** - Customer must have a default payment method in Shopify - Payment method must be valid and not expired - Updates are reflected immediately for future billing - Does not retry failed billing attempts automatically **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update minimum billing cycles for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-minimum-billing-cycles-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-min-cycles Sets the minimum number of billing cycles (payments) required before a customer can cancel their membership. This is commonly used to enforce commitment periods or prevent early cancellations. **Key Features:** - **Commitment Enforcement**: Require customers to stay subscribed for a minimum period - **Cancellation Prevention**: Blocks cancellation until minimum cycles are met - **Contract Terms**: Implements contractual minimum billing requirements - **Flexible Duration**: Set any number from 0 (no minimum) to higher values **Common Use Cases:** - **3-Month Minimum**: Set minCycles=3 for quarterly commitment - **Annual Contract**: Set minCycles=12 for yearly memberships - **Trial Completion**: Require 1-2 cycles before allowing cancellation - **Remove Restriction**: Set minCycles=0 or null to remove minimum **Behavior:** - Customer portal will show "X cycles remaining until cancellation allowed" - Cancellation button disabled until minimum met - Does not affect pausing memberships - Applies to future billing cycles, not retroactive **Parameters:** - **contractId** (required): The membership contract ID - **minCycles** (optional): Number of minimum cycles (null or 0 = no minimum) **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update order note/instructions for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-order-noteinstructions-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-order-note/{contractId} Updates the persistent order note attached to a membership contract. This note is automatically included with **EVERY future recurring order** generated from this membership, appearing in Shopify admin order details and printable packing slips. **IMPORTANT: Recurring vs One-Time Notes** - **This Endpoint (Contract Note)**: Applies to **ALL FUTURE ORDERS** permanently - **Billing Attempt Note** (`/subscription-billing-attempts-update-order-note`): Applies to **ONE SPECIFIC ORDER** only - **Combined Behavior**: If both notes exist, they are concatenated in the Shopify order **How It Works:** 1. Accepts membership contract ID and new order note text 2. Stores note in membership contract record 3. Every time a new order is created (monthly, weekly, etc.), note is automatically added 4. Previous contract note is replaced (not appended) 5. Empty string clears the existing note 6. Does NOT affect past orders already created **Character Limits & Validation:** - **Maximum Length**: 5000 characters (Shopify order note limit) - **Encoding**: UTF-8 supported (emojis, international characters allowed) - **HTML**: Plain text only - HTML tags display as text - **Line Breaks**: Use `\n` for line breaks (preserved in Shopify) - **Special Characters**: Automatically escaped for safety **Common Use Cases:** **1. Permanent Delivery Instructions** ``` Example: "Always leave package at back door. Do not ring doorbell (baby sleeping)." Use Case: Customer wants same delivery instructions for all future orders Applies To: Every monthly shipment permanently ``` **2. Gift Membership Messages** ``` Example: "This is a gift membership for Mom. Happy Birthday! Love, Sarah" Use Case: Gift membership with recurring message Applies To: All orders until membership ends or note is changed ``` **3. Special Handling Requirements** ``` Example: "FRAGILE - Glass bottles. Handle with care. Keep upright during shipping." Use Case: Delicate products requiring special warehouse handling Applies To: Every fulfillment automatically ``` **4. Customer Preferences** ``` Example: "Customer is allergic to peanuts. NO peanut products. Double-check packaging." Use Case: Critical dietary restrictions or preferences Applies To: All future orders for safety compliance ``` **5. Internal Merchant Notes** ``` Example: "VIP customer - priority processing. Include bonus samples." Use Case: Internal fulfillment team instructions Applies To: All shipments to provide consistent VIP treatment ``` **6. Clearing Unwanted Notes** ``` Example: orderNote="" (empty string) Use Case: Customer moved, no longer needs "Leave at neighbor" note Result: Future orders have no contract note (one-time notes still possible) ``` **Where Note Appears:** - **Shopify Admin**: Order details page under "Notes" - **Packing Slips**: Printed on warehouse packing slips (if enabled) - **Order Confirmation Emails**: May appear in customer emails (theme-dependent) - **Fulfillment Apps**: Visible to third-party logistics providers - **Order APIs**: Accessible via Shopify Order REST/GraphQL APIs **When to Use Contract Note vs Billing Attempt Note:** | Scenario | Use Contract Note | Use Billing Attempt Note | |----------|-------------------|-------------------------| | Permanent delivery instructions | ✅ | ❌ | | One-time special request | ❌ | ✅ | | Gift message for all shipments | ✅ | ❌ | | "Skip broccoli this week" | ❌ | ✅ | | Allergy warnings | ✅ | ❌ | | "Deliver to neighbor (vacation week)" | ❌ | ✅ | | VIP customer priority | ✅ | ❌ | **Error Handling:** **400 - Bad Request:** - Order note exceeds 5000 character limit - Contract ID doesn't belong to authenticated shop - Contract is in invalid state **404 - Contract Not Found:** - Membership contract ID doesn't exist - Contract was deleted - Wrong shop (contract belongs to different store) **Integration Best Practices:** 1. **Display Character Counter**: Show "450 / 5000 characters" in UI 2. **Preview Formatting**: Show how line breaks will appear 3. **Confirm Permanent Changes**: Warn user "This note will appear on ALL future orders" 4. **Sanitize Input**: Strip HTML tags, prevent injection attacks 5. **Show Current Note**: Pre-fill form with existing note before update **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update product variant in membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-product-variant-in-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contract-update-variant Replaces an existing product variant with a new variant in a membership contract. This allows customers to swap products in their membership while maintaining their membership. **Key Features:** - **Product Swapping**: Replace any line item's variant with a different variant - **Quantity Preservation**: Maintains the same quantity after variant swap - **Price Updates**: New variant pricing is applied automatically - **Flexible Identification**: Identify old variant by lineId OR variantId - **Activity Logging**: All variant changes are logged for audit trail **Identification Methods:** You can identify the old variant using either: - **oldLineId**: The Shopify line item ID from the membership contract - **oldVariantId**: The Shopify product variant ID At least one of these must be provided. **Common Use Cases:** - Customer wants to change product flavor/color/size in their membership - Swap discontinued products with new alternatives - Change product preferences mid-subscription - Update seasonal product selections - Replace out-of-stock variants with alternatives **Important Notes:** - **Immediate Effect**: Variant change applies to the next billing cycle - **Validation**: System validates new variant exists and is available - **Same Product**: Old and new variants can be from different products - **Price Changes**: Customer is charged the new variant's price **Variant ID Format:** - Accepts both numeric IDs and GraphQL IDs (gid://shopify/ProductVariant/...) - System automatically converts to correct format **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Update shipping address for membership contract Source: https://developers.appstle.com/memberships-admin-api/membership-contracts/update-shipping-address-for-membership-contract /memberships/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-shipping-address Changes the delivery shipping address for a membership contract. This allows customers to update where their membership orders are delivered. **Key Features:** - **Full Address Update**: Change complete shipping address in one call - **Address Validation**: System validates address format and country codes - **Immediate Effect**: New address applies to the next delivery - **Activity Logging**: All address changes are logged for audit trail - **International Support**: Supports addresses in all countries **Required Address Fields:** - **firstName**: Recipient's first name - **lastName**: Recipient's last name - **address1**: Street address line 1 - **city**: City name - **countryCode**: ISO 3166-1 alpha-2 country code (e.g., US, CA, GB) - **zip**: Postal/ZIP code **Optional Address Fields:** - **address2**: Apartment, suite, unit number - **company**: Company name (for business addresses) - **phone**: Contact phone number - **provinceCode**: State/province code (e.g., CA, NY, ON) **Common Use Cases:** - **Customer Moved**: Update address after relocation - **Temporary Address**: Ship to vacation home or temporary location - **Gift Recipient**: Change recipient for gift memberships - **Correct Typos**: Fix address errors from initial signup - **Business to Home**: Switch between business and residential addresses - **Seasonal Address**: Update for snowbird/seasonal residents **Country and Province Codes:** - **countryCode**: Use ISO 3166-1 alpha-2 codes - Examples: US (United States), CA (Canada), GB (United Kingdom), AU (Australia) - **provinceCode**: Use ISO 3166-2 subdivision codes - US Examples: CA (California), NY (New York), TX (Texas) - Canada Examples: ON (Ontario), BC (British Columbia), QC (Quebec) **Address Validation:** - **Format Validation**: System checks required fields are present - **Country Validation**: Verifies country code is valid - **Province Validation**: Checks province code matches country - **Postal Code**: Validates postal code format for country **Important Notes:** - **Shipping Rate Recalculation**: New address may have different shipping costs - **Delivery Method**: System automatically selects appropriate delivery method - **Next Order Only**: Only affects future orders, not orders already placed - **Address Book**: Consider updating customer's default address separately - **PO Boxes**: Some delivery methods may not support PO Box addresses **Shipping Cost Impact:** - Changing address may change shipping costs for future deliveries - International addresses typically have higher shipping costs - Remote/rural areas may have additional delivery fees - Consider notifying customer of cost changes **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get a membership plan group Source: https://developers.appstle.com/memberships-admin-api/membership-plans/get-a-membership-plan-group /memberships/admin-api-swagger.json get /api/external/v2/subscription-groups/{subscriptionGroupId} Retrieves a specific membership plan group by its Shopify selling plan group ID, including all plan configurations and assigned products. # List all membership plan groups Source: https://developers.appstle.com/memberships-admin-api/membership-plans/list-all-membership-plan-groups /memberships/admin-api-swagger.json get /api/external/v2/subscription-groups Retrieves all membership plan groups for the authenticated shop, including plan details, discount configurations, and product assignments. # Add one-time product to upcoming order Source: https://developers.appstle.com/memberships-admin-api/one-time-add-ons/add-one-time-product-to-upcoming-order /memberships/admin-api-swagger.json put /api/external/v2/subscription-contract-one-offs-by-contractId-and-billing-attempt-id Adds a one-time product (one-off) to a specific upcoming billing attempt for a membership contract. The product will be included only in the specified order and will not become part of the recurring membership. **Key Features:** - **One-Time Addition**: Product added to single order only - **Billing Cycle Targeting**: Specify exact order to add item to - **No Membership Impact**: Doesn't change recurring items - **Instant Upsell**: Add products between regular billing cycles - **Customer Flexibility**: Allow members to add extras to next order - **Activity Logging**: Tracks who added the item (merchant vs API) **Required Parameters:** - **Contract ID**: Target membership contract - **Billing Attempt ID**: Specific upcoming order to add item to - **Variant ID**: Shopify product variant to add - **Variant Handle**: Product handle for identification **How It Works:** 1. Validates contract exists and belongs to shop 2. Checks if contract is not frozen (minimum cycles) 3. Verifies billing attempt is upcoming (not already processed) 4. Adds variant to the specified billing attempt 5. Returns updated list of all one-offs for the contract 6. Logs activity for audit trail **Use Cases:** - Customer wants to add a bonus product to next delivery - Merchant offers limited-time add-on to existing members - Trial/sample products added to specific orders - Holiday specials or seasonal items - Promotional gifts or rewards - One-time upsells in customer portal **Business Rules:** - Item added only to specified billing attempt - Cannot add to past or completed orders - Contract must not be frozen/minimum cycle locked - Duplicate items increment quantity **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get one-time add-ons for a membership contract Source: https://developers.appstle.com/memberships-admin-api/one-time-add-ons/get-one-time-add-ons-for-a-membership-contract /memberships/admin-api-swagger.json get /api/external/v2/subscription-contract-one-offs-by-contractId Retrieves all one-time product additions scheduled for upcoming orders of a specific membership contract. One-offs are products added to a single billing cycle without affecting the recurring membership items. **Key Information Returned:** - **One-Off Items**: List of products added to upcoming orders - **Product Details**: Variant ID, handle, title, price, image - **Billing Association**: Which billing attempt the item is added to - **Quantity**: Number of units for the one-time add-on - **Status**: Active, pending, or fulfilled one-offs - **Created Date**: When the one-off was added **One-Off Item Data:** - **Contract ID**: Parent membership contract - **Billing Attempt ID**: Specific order the item will be added to - **Variant Info**: Product variant being added - **Pricing**: One-time price for the add-on - **Fulfillment Status**: Whether item has been shipped **Use Cases:** - Display scheduled one-time add-ons in customer portal - Show customers which extra items are in next order - Sync one-off data with external order management systems - Generate custom order previews for customers - Build upsell dashboards showing add-on purchases - Track one-time product revenue per contract **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Remove one-time product from upcoming order Source: https://developers.appstle.com/memberships-admin-api/one-time-add-ons/remove-one-time-product-from-upcoming-order /memberships/admin-api-swagger.json delete /api/external/v2/subscription-contract-one-offs-by-contractId-and-billing-attempt-id Removes a previously added one-time product (one-off) from a specific upcoming billing attempt. This allows customers or merchants to cancel add-on items before the order is processed. **Key Features:** - **Flexible Cancellation**: Remove add-ons before order processes - **Targeted Removal**: Remove specific variant from specific order - **No Membership Impact**: Doesn't affect recurring items - **Customer Control**: Let members manage their add-ons - **Activity Logging**: Tracks removal for audit trail **Required Parameters:** - **Contract ID**: Target membership contract - **Billing Attempt ID**: Order to remove item from - **Variant ID**: Product variant to remove **How It Works:** 1. Validates contract exists and belongs to shop 2. Finds one-off matching contract, billing attempt, and variant 3. Deletes the one-off record 4. Returns updated list of remaining one-offs 5. Logs removal activity for audit trail **Use Cases:** - Customer changes mind about add-on product - Remove out-of-stock items from upcoming orders - Cancel promotional items no longer available - Customer wants to reduce order total - Merchant corrects mistakenly added items - Manage add-ons in customer portal **Business Rules:** - Can only remove from upcoming (unprocessed) orders - Must match exact contract, billing attempt, and variant - Returns remaining one-offs after deletion - Cannot remove from past/completed orders **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get product swap options for membership items Source: https://developers.appstle.com/memberships-admin-api/product-swap-rules/get-product-swap-options-for-membership-items /memberships/admin-api-swagger.json post /api/external/v2/product-swaps-by-variant-groups Retrieves available product swap/substitution options for specified variants based on configured swap rules. Returns multiple levels of swap suggestions allowing members to exchange products in their memberships. **Key Features:** - **Variant-Based Swaps**: Get swap options for specific product variants - **Multi-Level Suggestions**: Returns up to 4 levels of swap alternatives - **Quantity Preservation**: Maintains quantities when suggesting swaps - **Recurring Order Rules**: Only returns swaps configured for every recurring order - **Group-Based Matching**: Uses variant groups to find compatible swaps - **Product Enrichment**: Includes product details (title, price, images) **How Swap Rules Work:** 1. Merchant configures swap rules defining which products can substitute others 2. Rules are organized by variant groups (e.g., coffee roasts, tea flavors) 3. Each rule specifies "from" variants and "to" variants 4. Rules can be limited to specific membership frequencies or all orders 5. Members can swap products within the allowed groups **Request Structure:** - **Variant Quantity List**: Array of variant IDs with quantities - Each entry contains variant ID and quantity - System finds swap options for all provided variants **Response Structure:** - Returns nested list of swap options (up to 5 levels) - Level 0: Original variants (as provided in request) - Levels 1-4: Progressive swap suggestions - Each level contains variant IDs, quantities, titles, prices, images - Variants appear with full Shopify product data **Use Cases:** - Display product swap options in customer portal - Allow members to switch between flavor/size variants - Build custom product selection interfaces - Enable seasonal product swaps (summer/winter varieties) - Offer alternative products when items are out of stock - Let members customize memberships within allowed product groups **Example Scenario:** Member has "Dark Roast Coffee (12oz)" in membership. API returns: - Level 0: Dark Roast Coffee 12oz (original) - Level 1: Medium Roast Coffee 12oz, Light Roast Coffee 12oz - Level 2: Dark Roast Coffee 16oz, Decaf Dark Roast 12oz - Level 3: Espresso Blend 12oz, French Roast 12oz - Level 4: Additional variants based on swap rules **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Create shipping/delivery profile (V1 format - Legacy) Source: https://developers.appstle.com/memberships-admin-api/shipping-&-delivery-profiles/create-shippingdelivery-profile-v1-format--legacy /memberships/admin-api-swagger.json post /api/external/v2/delivery-profiles/create-shipping-profile Creates a new Shopify delivery profile with shipping zones and rates using the legacy V1 format. For new integrations, prefer the V2 endpoint which supports more advanced configuration. **Key Features:** - **Single Location**: Configure shipping for primary store location - **Country-Based Zones**: Define shipping zones by country - **Fixed Rates**: Set flat shipping costs per zone - **Simple Configuration**: Straightforward setup for basic shipping needs **Configuration Structure:** - **Profile Name**: Unique identifier for the delivery profile - **Country Codes**: List of countries in the shipping zone - **Shipping Rate**: Fixed price for the zone - **Method Name**: Display name for the shipping option **Use Cases:** - Simple country-based flat rate shipping - Basic free shipping configuration - Legacy system migrations - Single-warehouse operations **Migration Note:** Consider using the V2 endpoint (`/api/external/v2/delivery-profiles/v2/create-shipping-profile`) for enhanced features like multi-location support, province targeting, and flexible rate types. **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Create shipping/delivery profile (V2 format) Source: https://developers.appstle.com/memberships-admin-api/shipping-&-delivery-profiles/create-shippingdelivery-profile-v2-format /memberships/admin-api-swagger.json post /api/external/v2/delivery-profiles/v2/create-shipping-profile Creates a new Shopify delivery profile with custom shipping zones, rates, and methods. Delivery profiles control which shipping options are available to customers at checkout for membership orders. **What is a Delivery Profile?** A delivery profile defines shipping configurations (zones, rates, methods) for specific products or locations. Membership memberships can have dedicated delivery profiles with custom shipping pricing (free shipping for VIPs, regional rates, express delivery options, etc.). **Request Body Structure (CreateShippingProfileRequestV2):** ```json { "profileName": "VIP Membership Free Shipping", "locationInfos": [ { "locationId": "gid://shopify/Location/12345", "countryInfos": [ { "countryCode": "US", "provinceCodeInfoList": ["CA", "NY", "TX"], "deliveryMethodInfo": [ { "name": "Standard Shipping", "amount": 0.00, "currencyCode": "USD", "minWeight": null, "maxWeight": null, "description": "Free for members" }, { "name": "Express Shipping", "amount": 9.99, "currencyCode": "USD", "description": "2-day delivery" } ] } ] } ] } ``` **Field Explanations:** **profileName** (string, required): - Unique name for the delivery profile - Visible in Shopify admin - Examples: "Premium Member Shipping", "International Free Shipping", "Express Delivery Profile" - Max length: 255 characters **locationInfos** (array, required): - Array of store locations this profile applies to - Get location IDs via Shopify Admin API or `/data/locations` endpoint - Can configure different shipping for each warehouse/store - **locationId** format: `gid://shopify/Location/{numeric_id}` **countryInfos** (array, required): - Shipping configuration per country - **countryCode**: ISO 3166-1 alpha-2 code ("US", "CA", "GB", etc.) - **provinceCodeInfoList**: Optional state/province filtering (e.g., ["CA", "NY"] for US states) - Omit to include all provinces/states - Use ISO 3166-2 province codes (e.g., "CA" for California, "ON" for Ontario) **deliveryMethodInfo** (array, required): - Shipping methods available for this zone - **name**: Method display name ("Standard", "Express", "Overnight") - **amount**: Shipping cost (0.00 for free shipping) - **currencyCode**: Auto-set to shop currency (do not manually specify) - **minWeight** / **maxWeight**: Optional weight restrictions in grams - **description**: Optional customer-facing description **Common Use Cases & Examples:** **1. Free Shipping for VIP Members** ```json { "profileName": "VIP Free Shipping", "locationInfos": [{ "locationId": "gid://shopify/Location/67890", "countryInfos": [{ "countryCode": "US", "deliveryMethodInfo": [{ "name": "Free Standard Shipping", "amount": 0.00, "description": "Included with VIP membership" }] }] }] } ``` **2. Regional Pricing (Different Rates per State)** ```json { "profileName": "Regional Shipping", "locationInfos": [{ "locationId": "gid://shopify/Location/12345", "countryInfos": [ { "countryCode": "US", "provinceCodeInfoList": ["CA", "OR", "WA"], "deliveryMethodInfo": [{ "name": "West Coast Shipping", "amount": 4.99 }] }, { "countryCode": "US", "provinceCodeInfoList": ["NY", "NJ", "CT"], "deliveryMethodInfo": [{ "name": "East Coast Shipping", "amount": 5.99 }] } ] }] } ``` **3. Tiered Shipping (Standard + Express)** ```json { "profileName": "Multi-Speed Shipping", "locationInfos": [{ "locationId": "gid://shopify/Location/12345", "countryInfos": [{ "countryCode": "US", "deliveryMethodInfo": [ { "name": "Standard (5-7 days)", "amount": 0.00, "description": "Free standard shipping" }, { "name": "Express (2-3 days)", "amount": 9.99, "description": "Faster delivery" }, { "name": "Overnight", "amount": 24.99, "description": "Next business day" } ] }] }] } ``` **Validation Rules:** - **profileName**: Required, non-empty, max 255 characters - **locationInfos**: Must contain at least 1 location - **countryCode**: Must be valid ISO 3166-1 alpha-2 code - **provinceCodeInfoList**: Optional, must be valid province codes for the country - **deliveryMethodInfo**: Must have at least 1 method per country - **amount**: Must be >= 0 (cannot be negative) - **locationId**: Must exist in Shopify and be active **Common Errors:** **400 - Invalid Country Code:** ```json {"error": "Invalid country code 'USA'. Use 'US' instead (ISO 3166-1 alpha-2)"} ``` Solution: Use 2-letter codes (US, CA, GB, AU, etc.) **400 - Invalid Location ID:** ```json {"error": "Location not found: gid://shopify/Location/99999"} ``` Solution: Verify location exists via `/data/locations` endpoint **400 - Missing Delivery Methods:** ```json {"error": "Country US has no delivery methods configured"} ``` Solution: Add at least one delivery method to each country **400 - Invalid Province Code:** ```json {"error": "Province code 'California' invalid for US. Use 'CA'"} ``` Solution: Use 2-letter state codes (CA, NY, TX), not full names **500 - Shopify API Error:** Shopify's delivery profile API rejected the request. Common reasons: - Duplicate profile name - Missing required Shopify permissions - Shopify API rate limit exceeded - Invalid zone configuration **Response (DeliveryProfileDTO):** Returns created profile with: - `id`: Shopify delivery profile ID - `profileName`: Confirmed profile name - `active`: Whether profile is active (true by default) - `locationGroupId`: Shopify internal location group ID **How to Get Location IDs:** ``` GET /api/data/locations Response: { "locations": { "nodes": [ { "id": "gid://shopify/Location/12345", "name": "Main Warehouse" } ] } } ``` **Best Practices:** 1. **Test with One Country First**: Start with single country, add more after validation 2. **Use Descriptive Names**: "VIP Free Shipping" better than "Profile 1" 3. **Verify Location IDs**: Always fetch current locations before creating profile 4. **Set Reasonable Rates**: Research competitor shipping prices 5. **Provide Descriptions**: Help customers understand shipping options 6. **Weight Limits**: Use for heavy items requiring freight shipping **Authentication:** Requires API key authentication via X-API-Key header or api_key parameter # Get past orders Source: https://developers.appstle.com/memberships-customer-portal-api/billing-&-orders/get-past-orders /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-billing-attempts/past-orders Retrieves past order history for a membership contract from the customer portal. # Get upcoming orders Source: https://developers.appstle.com/memberships-customer-portal-api/billing-&-orders/get-upcoming-orders /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-billing-attempts/top-orders Retrieves upcoming/scheduled orders for a membership contract from the customer portal. # Skip an upcoming billing order Source: https://developers.appstle.com/memberships-customer-portal-api/billing-&-orders/skip-an-upcoming-billing-order /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-billing-attempts/skip-order/{id} Skips the next scheduled billing/order for a membership contract. The membership remains active, but the next billing date is moved to the following cycle. **Key Features:** - Skip next billing without canceling membership - Automatically reschedule to next billing cycle - Works for both regular and prepaid memberships - Activity logging for audit trail **How It Works:** 1. Validates billing attempt exists and belongs to shop 2. Updates next billing date to skip current cycle 3. Moves billing to the next scheduled interval 4. Logs activity (customer portal vs merchant portal) **Customer Portal Restrictions:** - Cannot skip if membership is frozen until min cycles - Validates contract ownership for security **Use Cases:** - Customer is traveling and wants to skip one delivery - Customer has excess inventory and wants to pause one cycle - Merchant wants to skip a billing due to out-of-stock items - Skip billing for special circumstances (holidays, etc.) **Prepaid Handling:** - Set `isPrepaid=true` for prepaid memberships - Different validation logic for prepaid vs pay-as-you-go **Authentication:** Requires authenticated shop user or customer portal token # Trigger billing attempt Source: https://developers.appstle.com/memberships-customer-portal-api/billing-&-orders/trigger-billing-attempt /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-billing-attempts/attempt-billing/{id} Triggers an immediate billing attempt for a specific membership billing record from the customer portal. # Update billing attempt Source: https://developers.appstle.com/memberships-customer-portal-api/billing-&-orders/update-billing-attempt /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-billing-attempts Updates a membership billing attempt record from the customer portal. # Get cancellation flow settings Source: https://developers.appstle.com/memberships-customer-portal-api/cancellation-flow-configuration/get-cancellation-flow-settings /memberships/storefront-api-swagger.json get /memberships/cp/api/cancellation-managements/{id} Retrieves the cancellation management configuration including retention offers, cancellation reasons, and survey settings. # Get custom CSS styles Source: https://developers.appstle.com/memberships-customer-portal-api/custom-css-styling/get-custom-css-styles /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-custom-csses/{id} Retrieves the custom CSS styling configuration for the membership customer portal and widgets. # Get past discount history Source: https://developers.appstle.com/memberships-customer-portal-api/customer-discount-history/get-past-discount-history /memberships/storefront-api-swagger.json get /memberships/cp/api/customer-discount-code-infos/get-past-discounts Retrieves the history of discount codes applied to a customer's membership contracts. # Get customer portal label translations for locale Source: https://developers.appstle.com/memberships-customer-portal-api/customer-portal-configuration/get-customer-portal-label-translations-for-locale /memberships/storefront-api-swagger.json get /memberships/cp/api/label-translations/locale # Get customer portal settings Source: https://developers.appstle.com/memberships-customer-portal-api/customer-portal-configuration/get-customer-portal-settings /memberships/storefront-api-swagger.json get /memberships/cp/api/customer-portal-settings/{id} Retrieves the customer portal configuration including UI customization, text labels, feature toggles, and branding options. # Create retention activity record Source: https://developers.appstle.com/memberships-customer-portal-api/customer-retention/create-retention-activity-record /memberships/storefront-api-swagger.json post /memberships/cp/api/customer-retention-activities Records a customer retention activity such as cancellation feedback, retention offer acceptance, or other retention-related actions. # Get loyalty customer details Source: https://developers.appstle.com/memberships-customer-portal-api/loyalty-integration/get-loyalty-customer-details /memberships/storefront-api-swagger.json get /memberships/cp/api/loyalty-integration/customer Retrieves loyalty program details for a customer associated with a specific membership contract, including points balance. # Get loyalty point earn options Source: https://developers.appstle.com/memberships-customer-portal-api/loyalty-integration/get-loyalty-point-earn-options /memberships/storefront-api-swagger.json get /memberships/cp/api/loyalty-integration/earn-options Retrieves available loyalty point earning campaigns and rules for the shop. # Get loyalty point redeem options Source: https://developers.appstle.com/memberships-customer-portal-api/loyalty-integration/get-loyalty-point-redeem-options /memberships/storefront-api-swagger.json get /memberships/cp/api/loyalty-integration/redeem-options Retrieves available loyalty point redemption rules and options for the shop. # Redeem loyalty points Source: https://developers.appstle.com/memberships-customer-portal-api/loyalty-integration/redeem-loyalty-points /memberships/storefront-api-swagger.json post /memberships/cp/api/loyalty-integration/redeem Redeems loyalty points for a customer associated with a specific membership contract using the specified redemption option. # Add line item to contract Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/add-line-item-to-contract /memberships/storefront-api-swagger.json put /memberships/cp/api/v2/subscription-contracts-add-line-item Adds a new product line item to a membership contract from the customer portal. # Apply cancellation retention discount Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/apply-cancellation-retention-discount /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-cancellation-discount Applies a retention discount to a membership contract as part of the cancellation flow to retain the customer. # Apply discount code Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/apply-discount-code /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-apply-discount Applies a discount code to a membership contract from the customer portal. # Cancel a membership contract Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/cancel-a-membership-contract /memberships/storefront-api-swagger.json delete /memberships/cp/api/subscription-contracts/{id} Permanently cancels an active membership contract in Shopify. This operation stops all future billing and marks the membership as CANCELLED. **Important Notes:** - **Permanent Action**: Cancellation cannot be undone. A new membership must be created to re-subscribe. - **Email Notification**: Cancellation email is automatically sent to the customer - **Activity Logging**: Cancellation is logged with reason/feedback for analytics - **Immediate Effect**: No future billing attempts will be made **Customer Portal Restrictions:** When called from customer portal, additional validations apply: - **Minimum Cycles**: Cannot cancel if membership hasn't completed minimum required billing cycles - **Frozen Memberships**: Cannot cancel memberships that are frozen until min cycle completion - **Attribute-Based Limits**: Respects any custom attribute-based cancellation restrictions **Cancellation Feedback:** - Optional `cancellationFeedback` parameter to capture why customer is cancelling - Common values: 'TOO_EXPENSIVE', 'NO_LONGER_NEEDED', 'SWITCHING_TO_COMPETITOR', 'OTHER' - Helps merchants understand churn reasons **Use Cases:** - Customer requests to cancel their membership - Merchant manually cancels membership (e.g., payment issues, customer request) - Automated cancellation after max cycles reached **Authentication:** Requires authenticated shop user or customer portal token # Cancel pending downgrade Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/cancel-pending-downgrade /memberships/storefront-api-swagger.json delete /memberships/cp/api/subscription-contract-details/{contractId}/pending-downgrade Cancels a pending plan downgrade for a membership contract. # Get current billing cycle Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-current-billing-cycle /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-details/current-cycle/{contractId} Retrieves the current billing cycle number for a specific membership contract. # Get current customer's membership profile Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-current-customers-membership-profile /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-customers # Get customer membership details Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-customer-membership-details /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-details/customer/{contractId} Retrieves membership contract customer details and order notes for a specific contract. # Get customer payment methods Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-customer-payment-methods /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-details/shopify/customer/{customerId}/payment-methods Retrieves available Shopify payment methods for a specific customer from the customer portal. # Get membership contract details Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-membership-contract-details /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contracts/contract/{contractId} Retrieves the raw membership contract details for a specific contract by ID. # Get pending downgrade Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-pending-downgrade /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-details/{contractId}/pending-downgrade Retrieves pending plan downgrade information for a membership contract. # Get raw Shopify membership contract data Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-raw-shopify-membership-contract-data /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contracts/contract-external/{contractId} # Get valid membership contracts for a customer Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/get-valid-membership-contracts-for-a-customer /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-customers-detail/valid/{id} # List current customer's valid membership contract IDs Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/list-current-customers-valid-membership-contract-ids /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-customers/valid # List Shopify fulfillments for a membership contract Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/list-shopify-fulfillments-for-a-membership-contract /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-details/subscription-fulfillments/{contractId} # Remove discount from contract Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/remove-discount-from-contract /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-remove-discount Removes an applied discount from a membership contract from the customer portal. # Remove line item from contract Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/remove-line-item-from-contract /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-remove-line-item Removes a product line item from a membership contract from the customer portal. # Send magic link email Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/send-magic-link-email /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contracts-email-magic-link Sends a magic link email to the customer for passwordless access to manage their membership. # Update billing interval Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-billing-interval /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-billing-interval Updates the billing frequency interval for a membership contract from the customer portal. # Update existing payment method details Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-existing-payment-method-details /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-existing-payment-method Updates the details of an existing payment method associated with a membership contract. # Update line item attributes Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-line-item-attributes /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-line-item-attributes Updates custom attributes on a membership contract line item from the customer portal. # Update line item variant Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-line-item-variant /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contract-update-variant Updates the product variant for a line item in a membership contract, allowing customers to swap to a different variant. # Update membership contract details Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-membership-contract-details /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contract-details Updates an existing membership contract's details from the customer portal. # Update membership contract status Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-membership-contract-status /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-status Updates the status of an existing membership contract (ACTIVE, PAUSED, CANCELLED, or EXPIRED). This is one of the most critical operations for managing membership lifecycles. **Supported Status Values:** - **ACTIVE**: Membership is active and will process recurring billing - **PAUSED**: Membership is temporarily paused, no billing will occur - **CANCELLED**: Membership is permanently cancelled, no future billing - **EXPIRED**: Membership has expired (typically used when max cycles reached) **Pause Duration:** - When pausing, you can optionally specify `pauseDurationCycle` to auto-resume after N cycles - If not specified, membership remains paused until manually reactivated **Business Rules & Validations:** - **Customer Portal Restrictions**: When called from customer portal, additional validations apply: - Cannot modify frozen memberships (freeze till min cycle condition) - Must respect billing cycle limits configured by merchant - **Contract Validation**: System validates that the contract belongs to the authenticated shop - **Activity Logging**: All status changes are logged with source (merchant portal vs customer portal) **Use Cases:** - Customer wants to pause their membership temporarily - Merchant needs to cancel a membership due to customer request - Reactivating a paused membership - Marking membership as expired when max cycles reached **Authentication:** Requires authenticated shop user or customer portal token # Update next billing date Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-next-billing-date /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-billing-date Updates the next billing date for a membership contract from the customer portal. # Update order note Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-order-note /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-order-note/{contractId} Updates the order note for a membership contract from the customer portal. # Update payment method Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-payment-method /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-payment-method Updates the payment method associated with a membership contract from the customer portal. # Update shipping address Source: https://developers.appstle.com/memberships-customer-portal-api/membership-contracts/update-shipping-address /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contracts-update-shipping-address Updates the shipping/delivery address for a membership contract from the customer portal. # Get all selling plans Source: https://developers.appstle.com/memberships-customer-portal-api/membership-plans/get-all-selling-plans /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-groups/all-selling-plans Retrieves all available selling plans for the customer portal, used to display plan options to customers. # Add or update one-off item Source: https://developers.appstle.com/memberships-customer-portal-api/one-time-add-ons/add-or-update-one-off-item /memberships/storefront-api-swagger.json put /memberships/cp/api/subscription-contract-one-offs-by-contractId-and-billing-attempt-id Adds or updates a one-time product for a specific membership contract's upcoming billing attempt. # Get one-off items for contract Source: https://developers.appstle.com/memberships-customer-portal-api/one-time-add-ons/get-one-off-items-for-contract /memberships/storefront-api-swagger.json get /memberships/cp/api/subscription-contract-one-offs-by-contractId Retrieves all one-time add-on products associated with a specific membership contract and billing attempt. # Get contextual pricing for a variant by currency Source: https://developers.appstle.com/memberships-customer-portal-api/product-&-inventory-data/get-contextual-pricing-for-a-variant-by-currency /memberships/storefront-api-swagger.json get /memberships/cp/api/data/variant-contextual-pricing # Get product details by ID Source: https://developers.appstle.com/memberships-customer-portal-api/product-&-inventory-data/get-product-details-by-id /memberships/storefront-api-swagger.json get /memberships/cp/api/data/product # Get product with selling plans and variants Source: https://developers.appstle.com/memberships-customer-portal-api/product-&-inventory-data/get-product-with-selling-plans-and-variants /memberships/storefront-api-swagger.json get /memberships/cp/api/data/product-selling-plan-variant # List products attached to selling plans Source: https://developers.appstle.com/memberships-customer-portal-api/product-&-inventory-data/list-products-attached-to-selling-plans /memberships/storefront-api-swagger.json get /memberships/cp/api/data/selling-plan-products # Search membership-enabled products with pagination Source: https://developers.appstle.com/memberships-customer-portal-api/product-&-inventory-data/search-membership-enabled-products-with-pagination /memberships/storefront-api-swagger.json get /memberships/cp/api/data/products # Get product swap options by variant Source: https://developers.appstle.com/memberships-customer-portal-api/product-swap-rules/get-product-swap-options-by-variant /memberships/storefront-api-swagger.json post /memberships/cp/api/product-swaps-by-variant-groups Retrieves available product swap/substitution options for specified variant IDs, allowing customers to exchange membership items. # Get pickup locations Source: https://developers.appstle.com/memberships-customer-portal-api/shipping-&-delivery-profiles/get-pickup-locations /memberships/storefront-api-swagger.json get /memberships/cp/api/delivery-profiles/get-locations Retrieves available pickup locations for the customer's membership delivery. # Get shop info for current user Source: https://developers.appstle.com/memberships-customer-portal-api/shop-settings/get-shop-info-for-current-user /memberships/storefront-api-swagger.json get /memberships/cp/api/shop-infos-by-current-login Retrieves shop information and membership settings for the currently authenticated customer's shop. # Authenticate with the Appstle Memberships API Source: https://developers.appstle.com/memberships/authentication Create API keys in your Appstle dashboard and pass them via the X-API-Key header. Manage up to 10 keys per store, each independently revocable. Every Admin API request must include a valid API key in the request header. Keys are created and managed directly in your Appstle dashboard, scoped to a single Shopify store, and can be revoked individually without affecting your other integrations. ## Creating an API key Log in to your Appstle admin panel and navigate to **Settings → API Key Management**. Click **Create New Key** and give it a descriptive name that identifies the integration — for example, `Klaviyo Integration`, `Mobile App`, or `Zapier Workflow`. Your new key is displayed **only once**. Copy it now and store it in a secure location such as an environment variable or a secrets manager. You cannot retrieve the full key value again after leaving this screen. Pass the key in the `X-API-Key` header of every Admin API request. Never expose your API key in client-side JavaScript, browser applications, or public source code repositories. API keys must only be used in server-side code. ## Sending the API key Include your key in the `X-API-Key` request header: ```bash cURL theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts?shop=your-store.myshopify.com&customerId=12345" \ -H "X-API-Key: apst_your-api-key-here" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://membership-admin.appstle.com/api/external/v2/membership-contracts?shop=your-store.myshopify.com&customerId=12345', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); const data = await response.json(); ``` ```python Python theme={null} import requests import os response = requests.get( 'https://membership-admin.appstle.com/api/external/v2/membership-contracts', params={'shop': 'your-store.myshopify.com', 'customerId': '12345'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) data = response.json() ``` You can also pass the key as a query parameter using `?api_key=apst_your-api-key-here`, but the header approach is recommended for security. ## Key format All API keys use the `apst_` prefix. Existing legacy keys created before this prefix was introduced continue to work without any migration required. ``` apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012 ``` ## Key management You can create up to **10 active API keys** per store. Managing keys from the dashboard gives you fine-grained control over which integrations can access your membership data. | Action | How | | ---------- | ------------------------------------------------------------------------ | | **Create** | Settings → API Key Management → Create New Key | | **Track** | Each key displays its last-used timestamp so you can identify stale keys | | **Revoke** | Click Revoke on any individual key — other keys are not affected | | **Rotate** | Create a new key, update your integration, then revoke the old key | Create a separate API key for each integration. This way you can revoke access for a single integration without disrupting others — for example, if a third-party tool is compromised or decommissioned. ## Storing keys securely Store your API key as an environment variable and read it at runtime. Never hardcode it in your source files. ```bash theme={null} # .env — never commit this file APPSTLE_API_KEY=apst_your-api-key-here ``` ```javascript theme={null} // Read from environment at runtime const apiKey = process.env.APPSTLE_API_KEY; ``` ## Authentication errors If your request is rejected due to authentication, you will receive a `401 Unauthorized` response: ```json theme={null} { "error": "Unauthorized", "message": "Invalid API key provided", "status": 401 } ``` Common causes: * The key was revoked from the dashboard * The key was copied with extra whitespace * The `X-API-Key` header is missing from the request * You are using a key that belongs to a different store ## Partner integrations If you are building a product that integrates with Appstle Memberships on behalf of multiple merchants (a CRM, helpdesk, email platform, or automation tool), use the [Partner Integration Framework](/memberships/partner-integration). Merchants connect your app with one click — no manual key exchange — and Appstle issues your app a scoped `apst_...` token for each approved store. Send the merchant's scoped token as `X-API-Key`, exactly like a regular API key. Tokens are revoked automatically when a merchant disconnects or uninstalls Appstle. To get onboarded, email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email. Partner integrations bypass the paid API plan — merchants are never charged for your integration's API usage. Direct API access (without a partner connection) requires an active API plan. # Appstle Memberships third-party integration guide Source: https://developers.appstle.com/memberships/integration-guide Integrate with Appstle Memberships: authentication, base URL, membership management, member data, access control, billing, and common integration patterns. This guide covers everything you need to build a robust integration with Appstle Memberships. It includes authentication setup, the key endpoints for common workflows, and practical patterns for CRMs, email platforms, access control systems, and analytics tools. ## Authentication ### Merchant API key For direct API access, pass the merchant's API key in every request header: ``` X-API-Key: ``` Merchants create and manage API keys in the Appstle dashboard under **Settings → API Key Management**. Each key is scoped to a single Shopify store and can be individually revoked. Direct API access requires an active API plan. Contact [support@appstle.com](mailto:support@appstle.com) for pricing. ### Partner integrations If you are building a product that integrates with Appstle Memberships on behalf of multiple merchants — a CRM, helpdesk, email platform, or automation tool — use the [Partner Integration Framework](/memberships/partner-integration). Partner integrations bypass the paid plan requirement entirely. Merchants connect your app with one click, and Appstle issues your app a scoped `apst_...` token per approved store. Send it as `X-API-Key`, exactly like a regular API key — it is revoked automatically when the merchant disconnects. To get onboarded, email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email. ## Base URL ``` https://membership-admin.appstle.com ``` All external endpoints are prefixed with `/api/external/v2/`. ## Membership management ### List membership contracts for a customer Retrieve all membership contracts for a specific customer. This is the most common first call in any integration. ```bash theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts?shop=your-store.myshopify.com&customerId=12345" \ -H "X-API-Key: YOUR_API_KEY" ``` **Response:** ```json theme={null} { "content": [ { "id": 1001, "status": "ACTIVE", "membershipPlanName": "Gold Member", "nextBillingDate": "2026-03-01", "billingCycleType": "MONTHLY", "startDate": "2026-01-01", "endDate": null } ], "totalElements": 1 } ``` ### Check membership status Quickly verify whether a customer is an active member before granting access to gated content or applying member discounts: ```bash theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-status?shop=your-store.myshopify.com&customerId=12345" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Get a specific contract Retrieve full details for a single membership contract by its ID: ```bash theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts/{contractId}?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ### List membership plans Retrieve all membership plans available in a store: ```bash theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-plans?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Billing operations ### Cancel a membership Cancel a customer's membership programmatically: ```bash theme={null} curl -X POST \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts/{contractId}/cancel?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reason": "Customer requested cancellation via support ticket"}' ``` ### Pause a membership Pause a membership contract: ```bash theme={null} curl -X POST \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts/{contractId}/pause?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Resume a paused membership Reactivate a membership that was previously paused: ```bash theme={null} curl -X POST \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts/{contractId}/resume?shop=your-store.myshopify.com" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Common integration patterns Help agents verify and manage memberships directly while handling support tickets: 1. Look up member status by customer ID from the Shopify customer context 2. Display active plan name, billing date, and contract status in the agent sidebar 3. Cancel, pause, or update memberships directly from the helpdesk with a single API call 4. Log actions back for audit trail purposes **Key endpoints:** `GET /membership-contracts`, `GET /membership-status`, `POST /{contractId}/cancel`, `POST /{contractId}/pause` Sync membership data for lifecycle automations: 1. Use [webhooks](/memberships/webhooks) to receive real-time membership events (`membership.created`, `membership.cancelled`, etc.) 2. Enrich customer profiles with membership tier, plan name, and next billing date 3. Trigger flows based on events: * `membership.billing-failure` → "Update your payment method" dunning sequence * `membership.cancelled` → Win-back offer sequence * `membership.created` → Welcome onboarding sequence * `membership.expired` → Re-engagement campaign **Key endpoints:** `GET /membership-contracts`, webhook events Control access to content, pages, or products based on active membership: 1. On page load, call the membership status endpoint from your server-side code 2. Check that `status === "ACTIVE"` and that the plan grants access to the requested resource 3. Gate or unlock accordingly — redirect non-members to the plan selection page 4. Cache membership status per session to stay within rate limits **Key endpoints:** `GET /membership-status`, `GET /membership-contracts` Pull membership data for reporting and revenue projections: 1. Use the membership contracts list endpoint with pagination to export all contracts 2. Filter by `status` to track active vs. churned members 3. Use `nextBillingDate` and `billingCycleType` for MRR/ARR projections 4. Combine with [webhook events](/memberships/webhooks) to track churn in real time **Key endpoints:** `GET /membership-contracts` (paginated), `GET /membership-plans`, webhook events ## Rate limits API requests are rate-limited per store. If you receive a `429 Too Many Requests` response, implement exponential backoff before retrying. Cache membership status per customer session where possible to reduce the number of API calls. ## Response format All responses use standard HTTP status codes: | Status | Meaning | | ------ | ----------------------------------------- | | `200` | Success | | `201` | Created | | `400` | Bad Request — invalid parameters | | `401` | Unauthorized — invalid or missing API key | | `403` | Forbidden — key lacks permission | | `404` | Not Found | | `429` | Too Many Requests — rate limit exceeded | | `500` | Server Error | ## Resources Receive real-time event notifications for membership lifecycle and billing events. Automate membership workflows without writing code using Shopify Flow triggers. Shopify metafields and customer tags used for membership access control. # Appstle Memberships: Tiered Plans & Access Control Source: https://developers.appstle.com/memberships/introduction Appstle Memberships lets you build recurring membership programs on Shopify with tiered plans and gated content — via Admin and Customer Portal APIs. Appstle Memberships gives you programmatic access to every part of your membership program — from creating tiered plans and gating content to processing renewals and tracking analytics. Whether you're connecting a CRM, building a mobile app, or customizing your storefront member portal, the APIs deliver the flexibility you need. ## Available APIs Appstle Memberships exposes two REST API surfaces. Pick the one that matches where your code runs: * **Admin API** — server-side, authenticated with `X-API-Key`. For backend integrations, admin dashboards, bulk operations, mobile apps, and third-party sync. Browse the full reference under **Admin API** in the sidebar. * **Customer Portal API** — customer-facing, accessed through Shopify App Proxy. For member self-service portals, theme integrations, and gated content. Browse the full reference under **Customer Portal API** in the sidebar. ## Which API should you use? * You are building **server-side integrations** and automation workflows * You are **managing memberships** from your backend or a CRM * You are building **admin dashboards** or reporting tools * You are automating **bulk operations** across your member base * You are integrating with **email platforms, analytics tools, or helpdesks** * You are building a **mobile app** (iOS or Android) Admin API requests require an `X-API-Key` header. Keys are created in the Appstle dashboard under **Settings → API Key Management**. Never expose keys in client-side code. * You are building a **custom member portal** embedded in your theme * You want members to **self-manage their membership** (pause, cancel, upgrade) * You are gating **content that requires an active membership** to view * You are building **storefront pages** that display plan details and billing dates Customer Portal API endpoints run exclusively through Shopify's App Proxy and require the customer to be logged in. They will not accept API key authentication and cannot be called from a backend server — use the Admin API for any integration outside your storefront. ## Key features Create and manage tiered membership plans with flexible pricing and billing cycles — monthly, annual, or custom intervals. Retrieve and sync member data, plan status, and access entitlements across your systems in real time. Gate content, collections, and pricing based on membership status and tier using customer tags and metafields. Manage membership contracts, renewals, and billing attempts. Handle dunning, pauses, and cancellations programmatically. Configure perks, discounts, and exclusive access rules per membership tier. Swap plans and roll back automatically on failed payments. Automate member communications for renewals, expirations, billing failures, and upgrades via Shopify Flow or webhooks. Access membership metrics, churn data, MRR projections, and revenue analytics using contract list endpoints and billing events. Deep integration with Shopify Flow, metafields, customer tags, and the App Proxy — no separate infrastructure required. ## Base URL All Admin API endpoints use: ``` https://membership-admin.appstle.com/api/external/v2/ ``` Customer Portal API endpoints are accessed through your store's Shopify App Proxy — no separate base URL is needed. ## HTTP status codes All API responses use standard HTTP status codes. | Code | Meaning | | ----- | ------------------------------------------ | | `200` | Success | | `201` | Resource created | | `400` | Bad request — invalid parameters | | `401` | Unauthorized — missing or invalid API key | | `403` | Forbidden — key lacks required permissions | | `404` | Not found | | `429` | Rate limit exceeded | | `500` | Server error | Error responses follow this shape: ```json theme={null} { "error": "Unauthorized", "message": "Invalid API key provided", "status": 401 } ``` ## Next steps Create your API key and learn how to authenticate requests. Make your first API call in under five minutes. Explore common integration patterns for CRMs, email platforms, and access control. # Shopify metafields and tags for Appstle Memberships Source: https://developers.appstle.com/memberships/metafields-and-tags Shopify metafields and tags set by Appstle Memberships — what they contain, when they update, and how to use them for storefront gating and integrations. Appstle Memberships uses Shopify metafields and customer tags to store membership data, power storefront access gating, and drive checkout validation. This reference covers every metafield and tag — what it contains, when it is set, and how to use it in your integration or Liquid theme. All metafields use the namespace `appstle_membership` without a `$app:` prefix, which means they are publicly readable by other apps, themes, and Liquid templates. ## Metafields overview Metafields are set on three Shopify resource types: | Resource | Keys | Visibility | Description | | ------------ | ---- | ---------- | ------------------------------------------------------------------------------------ | | **Shop** | 6 | Public | Membership settings, selling plans, access rules, checkout validation, widget labels | | **Customer** | 2 | Public | Membership contracts, trial and dunning state | | **Order** | 1 | Public | Membership contract context for each order | ## Shop metafields Shop metafields store the membership program configuration and are used by the storefront for access gating, checkout validation, and widget rendering. They are updated synchronously on every settings save in the Appstle admin. ### `appstle_membership` / `setting` Stores shop-level membership settings as a snapshot for storefront access. Used by the app and themes to determine global membership behavior. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ### `appstle_membership` / `all_selling_plans` All selling plans configured for this store's membership program. Themes and apps read this to display available membership options. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ```json theme={null} [ { "id": "gid://shopify/SellingPlan/111", "name": "Basic Monthly Membership", "billingPolicy": { "interval": "MONTH", "intervalCount": 1 }, "customerTag": "basic-member", "orderTag": "membership-order" }, { "id": "gid://shopify/SellingPlan/222", "name": "Premium Annual Membership", "billingPolicy": { "interval": "YEAR", "intervalCount": 1 }, "customerTag": "premium-member", "orderTag": "premium-membership-order" } ] ``` ### `appstle_membership` / `rules_by_customer_tag` Access rules keyed by customer tag. The storefront reads this metafield alongside the customer's tags to determine which collections and products each membership tier can access. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ```json theme={null} { "basic-member": { "accessibleCollections": ["gid://shopify/Collection/111"], "accessibleProducts": [], "gatingType": "COLLECTION" }, "premium-member": { "accessibleCollections": ["gid://shopify/Collection/111", "gid://shopify/Collection/222"], "accessibleProducts": ["gid://shopify/Product/333"], "gatingType": "COLLECTION_AND_PRODUCT" } } ``` If a customer has the `premium-member` tag, they see all collections and products mapped to that tag. Non-members or lower tiers see gated content as locked or hidden, depending on your theme configuration. ### `appstle_membership` / `checkout_validation` Checkout validation configuration. Enforces member-only product purchase rules at checkout. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ### `appstle_membership` / `widget_label` Storefront widget label translations for the default locale. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ### `appstle_membership` / `customer_portal_label` Customer portal label translations for the default locale. For non-default locales, translations are registered via Shopify's `TranslationsRegisterMutation` rather than separate metafields. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Shop | ## Customer metafields Customer metafields are updated asynchronously whenever a membership contract changes. Updates are queued and processed within a few seconds, though processing may take longer during high-traffic periods. ### `appstle_membership` / `subscriptions` All membership contracts for this customer with full details. Updated whenever any contract for the customer changes — created, updated, paused, cancelled, or billed. | Property | Value | | -------- | -------- | | Type | `json` | | Resource | Customer | ```json theme={null} [ { "id": "gid://shopify/SubscriptionContract/9876543210", "status": "ACTIVE", "sellingPlanIds": ["gid://shopify/SellingPlan/111"], "sellingPlanNames": ["Premium Monthly Membership"], "variantIds": ["gid://shopify/ProductVariant/222"], "variantNames": ["Premium Membership"], "nextBillingDate": "2025-04-15T10:30:00Z" } ] ``` ### `appstle_membership` / `setting` Customer-level settings tracking trial and dunning state for this customer's active plans. | Property | Value | | -------- | -------- | | Type | `json` | | Resource | Customer | ```json theme={null} { "trialTags": "basic-member,premium-member", "dunningTags": "premium-member" } ``` | Field | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `trialTags` | Comma-separated plan customer tags where the customer is currently in a free trial (no successful billing yet, within the trial window) | | `dunningTags` | Comma-separated plan customer tags where the customer has a recent failed billing attempt requiring retry | The key `setting` is used for both Shop-level and Customer-level metafields, but they contain different data structures. The resource type (Shop vs. Customer) distinguishes them. ## Order metafields ### `appstle_membership` / `details` Full membership contract context at the time the order was created. Set on both initial purchase orders and recurring renewal orders. | Property | Value | | -------- | ------ | | Type | `json` | | Resource | Order | ```json theme={null} { "customer": { "id": "gid://shopify/Customer/1234567890", "name": "Jane Smith", "email": "jane@example.com" }, "subscriptionContract": { "id": "gid://shopify/SubscriptionContract/9876543210", "status": "ACTIVE", "sellingPlanIds": ["gid://shopify/SellingPlan/111"], "sellingPlanNames": ["Premium Monthly Membership"], "variantIds": ["gid://shopify/ProductVariant/222"], "variantNames": ["Premium Membership"] }, "firstOrder": { "id": "gid://shopify/Order/444", "createdAt": "2025-01-15T10:30:00Z" } } ``` ## Customer tags Customer tags are the primary mechanism for membership access control. Each membership plan has a merchant-configured `customerTag` that is added to a customer's Shopify profile when their contract is active. ### Plan-based customer tags | Property | Value | | ------------- | -------------------------------------------------------------------------------- | | Format | Free-form string per plan (e.g., `basic-member`, `premium-member`, `vip-member`) | | Configured in | Appstle admin — each membership plan's settings | #### Tag lifecycle | Event | Tag action | | -------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Contract becomes **ACTIVE** | Plan's `customerTag` is **added** | | Contract **CANCELLED** | Tag **removed** (immediately if `immediateTagRemoveOnCancel=true`, otherwise at `nextBillingDate`) | | Contract **PAUSED** | Tag **removed** (immediately if `immediateTagRemoveOnPause=true`, otherwise at `nextBillingDate`) | | Contract enters **DUNNING** (failed payment) | Tag **removed** | | Contract **RESUMED** | Tag **re-added** | #### Delayed vs. immediate tag removal | Setting | Default | Behavior | | ---------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `immediateTagRemoveOnCancel` | `false` | If `false`, the tag stays until `nextBillingDate` so the member retains access for the period they paid for. Set to `true` to remove access immediately. | | `immediateTagRemoveOnPause` | `false` | Same behavior for paused memberships. | #### Cross-membership protection When removing a tag on cancellation or pause, the app checks all other active contracts for the same customer. If another active contract uses the same `customerTag`, the tag is **not removed** — the customer retains access. For example: a customer holds "Basic Monthly" and "Basic Annual", both using the `basic-member` tag. Cancelling "Basic Monthly" does not remove the `basic-member` tag because "Basic Annual" is still active. ### Trial-state tags During a free trial, the plan's `customerTag` is applied — there is no separate trial tag. Trial members get identical access to paying members. ### Dunning-state tags When a billing attempt fails, the plan's `customerTag` may be removed and the tag is tracked in `dunningTags`. If a retry succeeds, the tag is re-added and access is restored automatically. ### Plan upgrade/downgrade tag swap When a member changes plans, tags are swapped atomically: 1. Old plan's `customerTag` is removed 2. New plan's `customerTag` is added 3. If the upgrade payment fails, the swap is rolled back: new tag removed, old tag restored ## Order tags ### Plan-based order tags Applied to orders on initial membership purchase and on each renewal billing. | Property | Value | | ------------- | ----------------------------------------------- | | Format | Free-form string per plan | | Configured in | Appstle admin — each membership plan's settings | | Removed | Never (order tags are permanent) | ### First-time order tag | Property | Value | | ------------ | ----------------------------------------- | | Config field | `ShopInfo.firstTimeOrderTag` | | Format | Liquid template string | | Applied when | Initial membership contract creation only | ### Recurring order tag | Property | Value | | ------------ | ------------------------------ | | Config field | `ShopInfo.recurringOrderTag` | | Format | Liquid template string | | Applied when | Each recurring billing attempt | ### Liquid template variables for order tags Both `firstTimeOrderTag` and `recurringOrderTag` support Liquid template syntax: | Variable | Type | Example | | ----------------------------- | ------ | ----------------------------------------- | | `{{customer.id}}` | String | `gid://shopify/Customer/1234567890` | | `{{subscriptionContract.id}}` | String | `gid://shopify/SubscriptionContract/9876` | | `{{firstOrder.id}}` | String | `gid://shopify/Order/444` | | `{{firstOrder.createdAt}}` | String | `2025-01-15T10:30:00Z` | Example — dynamic tag with contract info: ``` membership_{{subscriptionContract.id}} ``` Result: `membership_gid://shopify/SubscriptionContract/9876` ## Reading metafields in Liquid All membership metafields are readable in Liquid templates because they use the `appstle_membership` namespace without an `$app:` prefix: ```liquid theme={null} {{ shop.metafields.appstle_membership.setting }} {{ customer.metafields.appstle_membership.subscriptions }} {{ customer.metafields.appstle_membership.setting }} ``` ## Complete metafield reference | Resource | Namespace | Key | Type | | -------- | -------------------- | ----------------------- | ---- | | Shop | `appstle_membership` | `setting` | json | | Shop | `appstle_membership` | `all_selling_plans` | json | | Shop | `appstle_membership` | `rules_by_customer_tag` | json | | Shop | `appstle_membership` | `checkout_validation` | json | | Shop | `appstle_membership` | `widget_label` | json | | Shop | `appstle_membership` | `customer_portal_label` | json | | Customer | `appstle_membership` | `subscriptions` | json | | Customer | `appstle_membership` | `setting` | json | | Order | `appstle_membership` | `details` | json | ## Complete tag reference | Resource | Tag source | Applied when | Removed when | Permanent? | | -------- | ---------------------------- | -------------------------------- | ----------------------------------------------- | ---------- | | Customer | Plan `customerTag` | Contract ACTIVE or TRIAL | Contract CANCELLED, PAUSED, DUNNING, or EXPIRED | No | | Customer | Plan `customerTag` (upgrade) | Plan upgrade | Failed upgrade rollback | No | | Order | Plan `orderTag` | Order created (first or renewal) | Never | Yes | | Order | `firstTimeOrderTag` (Liquid) | First order only | Never | Yes | | Order | `recurringOrderTag` (Liquid) | Each renewal | Never | Yes | | Order | New plan's `orderTag` | Variant swap | Never | Yes | ## FAQ Yes. All membership metafields use the `appstle_membership` namespace without a `$app:` prefix, so they are accessible in Liquid via `{{ shop.metafields.appstle_membership.setting }}`, `{{ customer.metafields.appstle_membership.subscriptions }}`, and so on. By default (`immediateTagRemoveOnCancel=false`), the customer keeps their membership tag until `nextBillingDate`. They retain access for the period they have already paid for. Set `immediateTagRemoveOnCancel=true` to remove access immediately on cancellation. The app has cross-membership protection. When removing a tag due to cancellation or pause, it checks whether any other active contract for this customer uses the same tag. If so, the tag is not removed and the customer retains access. Customer metafield updates are queued and processed asynchronously. Typical latency is a few seconds, but during high-traffic periods it may take longer. Yes. During a free trial, the plan's `customerTag` is applied — there is no separate trial tag. Trial members get the same access as paying members. When a billing attempt fails, the plan's `customerTag` may be removed and the member loses access. The `dunningTags` field tracks which tags are in dunning. If a retry succeeds, the tag is re-added and access is restored automatically. # Partner Integration Framework overview Source: https://developers.appstle.com/memberships/partner-framework-overview How the Appstle Memberships Partner Integration Framework works — one handshake per merchant, a scoped API token, no API plan required, automatic revocation. The Partner Integration Framework lets your product — a CRM, helpdesk, email platform, or automation tool — connect to Appstle Memberships on behalf of many merchants. Instead of asking each merchant to create and paste an API key, your app completes a one-time handshake per store and receives a **scoped API token** for it. ## How a connection works You receive a **Partner ID** and **Partner Secret** used only for connection calls. Either from your product's UI, or from **Settings → Partner Connections** in their Appstle dashboard. Connections your app initiates stay pending until the merchant approves them in Appstle. Pending requests expire after 30 days. Appstle delivers a merchant-specific `apst_...` token to your callback. Send it as `X-API-Key` on Admin API calls — exactly like a regular API key. When a merchant disconnects your app — or uninstalls Appstle — the token is revoked immediately. ## Why use it * **No API plan required** — merchants are never billed for partner API usage * **One isolated token per merchant** — no shared credentials, no manual key exchange, individually revocable * **Merchant-controlled** — merchants see, approve, and disconnect partners from their own dashboard * **Automatic cleanup** — access is revoked the moment a merchant disconnects or uninstalls ## Access levels Your app's permission level is set during onboarding: | Permission | What your app can do | | ---------------- | --------------------------------------------------------------------------------------------- | | **Read Only** | View membership contracts, plans, billing history, and member status | | **Read & Write** | Everything above, plus create or cancel memberships, update billing, and manage member access | ## Connection modes | Mode | Use it when | Your app receives | | ----------------------------- | ------------------------------------------ | --------------------------------------------------- | | **Nonce Handshake** (default) | Your app needs to call Appstle's Admin API | A merchant-scoped `apst_...` API token | | **Simple Token Exchange** | Appstle should push data to *your* API | No Appstle token — Appstle stores a token you issue | ## Get started Email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email to get onboarded. Then follow the [Partner integration guide](/memberships/partner-integration) for the full implementation — endpoints, handshake, callbacks, and testing. # Partner Integration Framework Source: https://developers.appstle.com/memberships/partner-integration Build a seamless, zero-configuration integration between your app and Appstle Memberships using scoped API tokens and a nonce handshake. Build a seamless, zero-configuration integration between your app and Appstle Memberships. Once connected, your app gets a scoped API token for each merchant — no manual key exchange needed. **Why become a partner?** * Frictionless merchant onboarding — one-click connect from either dashboard * No API paywall — merchants don't need a paid API plan to use your integration * Scoped tokens — each merchant gets an isolated API key; revocable at any time * Automatic cleanup — when a merchant disconnects or uninstalls, access is revoked instantly ## How it works The Partner Integration Framework uses a secure handshake protocol. Either side — your app or Appstle — can initiate the connection. Both flows end with your app receiving a scoped API token. ```mermaid theme={null} sequenceDiagram autonumber participant P as Partner App participant A as Appstle Memberships rect rgb(240, 248, 255) Note over P,A: Flow A — Partner initiates P->>A: POST /api/partner/{id}/connect
(shop_domain, callback_nonce, secret) A->>P: POST {your_base_url}/appstle/verify
(nonce check) P-->>A: { "verified": true } A-->>P: { "status": "pending_merchant_approval" } Note over P,A: Merchant approves in Appstle dashboard A->>P: POST {your_base_url}/appstle/approved
(access_token) end rect rgb(245, 245, 250) Note over P,A: Flow B — Appstle initiates A->>P: POST {your_base_url}/appstle/connect
(shop_domain, app, callback_url, nonce) P->>A: POST /api/partner/{id}/verify
(shop_domain, callback_nonce, secret) A-->>P: { "access_token": "..." } end ``` **Merchant approval:** When your app initiates a connection (Flow A), the merchant must approve it from their Appstle dashboard before you receive an API token. When the merchant initiates from Appstle's side (Flow B), the connection is approved instantly because the merchant is the one clicking "Connect." ## Getting started ### Step 1: Get onboarded To get started, reach out to the Appstle team at [support@appstle.com](mailto:support@appstle.com) with the information below. Our team will set up your partner account and send you your credentials. #### What you'll need to provide | # | Field | Required? | Description | Example | | - | ------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | 1 | **App / Company Name** | Yes | Your app or company name. Displayed to merchants when they browse available partner integrations in the Appstle dashboard. | `SearchPie` | | 2 | **Partner ID** | Yes | A unique, lowercase slug that identifies your app in API URLs. Use only lowercase letters and hyphens. Once set, this cannot be changed. | `search-pie` | | 3 | **Base URL** | Yes | The HTTPS base URL where Appstle will send callback requests (connect, verify, approved). Must be publicly accessible — Appstle will not call HTTP or localhost URLs. | `https://api.searchpie.com` | | 4 | **Contact Email** | Yes | The email address where we'll send your Partner Secret and any onboarding follow-ups. Use a team email if possible — the secret is shown only once. | `dev-team@searchpie.com` | | 5 | **Authentication Mode** | Optional | How your API calls are authenticated. Choose one: **Partner Secret** (simpler — pass secret in a header) or **HMAC-SHA256** (more secure — sign each request). Defaults to Partner Secret if not specified. See [Step 1b](#step-1b-choose-your-authentication-mode) for details. | `Partner Secret` | | 6 | **Connect Mode** | Optional | How merchant connections are established. Choose one: **Nonce Handshake** (full two-way verification — you receive an Appstle API key) or **Simple Token Exchange** (streamlined — you provide your own token for Appstle to call your API). Defaults to Nonce Handshake if not specified. See [Step 1c](#step-1c-choose-your-connect-mode) for details. | `Nonce Handshake` | | 7 | **Custom Endpoint Paths** | Optional | By default, Appstle calls `/appstle/connect` and `/appstle/verify` on your Base URL. If you need different paths (e.g., `/webhooks/appstle/connect`), specify them here. | `/webhooks/appstle/connect` | | 8 | **Sync Path** | Optional | If you want Appstle to push membership data to your app (e.g., when memberships are created, cancelled, or plans change), provide the path on your server where Appstle should send these payloads. See [Data Sync](#data-sync-push-model) for details. | `/appstle/sync` | | 9 | **App Logo** | Optional | A square logo (PNG or SVG, at least 128×128px) displayed next to your app name in the merchant's Appstle dashboard. If not provided, a placeholder icon is used. | — | Not sure about some of these? Only the first four fields are required to get started. You can always reach out to [support@appstle.com](mailto:support@appstle.com) to change your authentication mode, connect mode, or add a sync path later. **Recommended default for new partners: Simple Token Exchange + Partner Secret.** This is the lowest-friction setup — your app generates a single access token per merchant, hands it to Appstle, and authenticates calls with an `X-Partner-Secret` header. No nonce storage, no HMAC computation, no `/appstle/verify` endpoint to implement. Pick this unless you specifically need Appstle to call your API on behalf of a merchant (use Nonce Handshake) or your security review mandates request signing (use HMAC-SHA256). #### API namespaces — what you call vs. what is internal Three URL namespaces appear in this codebase. **As a third-party partner, you only ever call the first one.** The others exist for Appstle's merchant portal and inter-app integrations and are documented here so the surface area is unambiguous: | Namespace | Who calls it | Purpose | | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/partner/...` | **You** (the partner) | Connect, verify, disconnect, status. Authenticated with your Partner Secret or HMAC. | | `/api/integrations/partner/...` | Appstle merchant-portal UI | Drives the merchant-facing "Connect / Disconnect" buttons in the Appstle dashboard. Session-authenticated; not part of the public partner API. | | `/api/integrations/callback/...` | Other Appstle apps | Receiver-side callbacks for app-to-app integrations between Appstle products. Not used by third-party partners. | If you see an example referring to `/api/integrations/...`, it's an internal Appstle flow and doesn't apply to your integration. #### What you'll receive Once onboarded, you'll receive three values: | Credential | Example | Description | | ------------------ | -------------------------------------- | ----------------------------------------------------------------------------- | | **Partner ID** | `search-pie` | Your unique identifier, as requested. Becomes part of the API URL. | | **Partner Secret** | `xK9mQ2vL...` (48 chars) | A secret key used to authenticate your API calls. Treat this like a password. | | **Base URL** | `https://membership-admin.appstle.com` | Appstle's API base URL. Same for all partners. | Your Partner Secret is shown only once during onboarding. Copy it immediately and store it in a secure location (environment variable, secrets manager, etc.). If you lose it, contact Appstle to rotate it — the old secret will be invalidated immediately. **Store your credentials as environment variables:** ```bash theme={null} # .env (never commit this file) APPSTLE_PARTNER_ID=search-pie APPSTLE_PARTNER_SECRET=xK9mQ2vLa8nR3pY... # if using Partner Secret auth APPSTLE_HMAC_KEY=your-hmac-key-here # if using HMAC-SHA256 auth APPSTLE_BASE_URL=https://membership-admin.appstle.com ``` ### Step 1b: Choose your authentication mode Appstle supports two ways to authenticate partner API calls. Your auth mode is configured during onboarding. #### Option A: Partner Secret (default) The simplest approach. Pass your secret in a header with every request: ```http theme={null} X-Partner-Secret: your-partner-secret ``` That's it. No computation needed. Good for getting started quickly. #### Option B: HMAC-SHA256 A more secure approach where requests are signed with a shared HMAC key. Instead of sending the secret directly, you compute a signature over the request body. **Headers required:** ```http theme={null} X-Partner-Timestamp: 1709856000 X-Partner-Signature: 5a3c1f2e9b8d7a6c... ``` **How to compute the signature:** 1. Get the current Unix timestamp (seconds, not milliseconds) 2. Concatenate the timestamp and the raw JSON request body: `timestamp + body` 3. Compute HMAC-SHA256 of that string using your HMAC key 4. Send the hex-encoded result in `X-Partner-Signature` **Timestamp validation:** Appstle rejects requests where the timestamp is more than 5 minutes from the server's current time. Make sure your server clock is synced (NTP). ```javascript Node.js theme={null} const crypto = require('crypto'); function signRequest(body, hmacKey) { const timestamp = Math.floor(Date.now() / 1000).toString(); const data = timestamp + body; const signature = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', }; } // Usage const body = JSON.stringify({ shop_domain: 'cool-store.myshopify.com' }); const headers = signRequest(body, process.env.APPSTLE_HMAC_KEY); ``` ```python Python theme={null} import hmac import hashlib import time import json def sign_request(body: str, hmac_key: str) -> dict: timestamp = str(int(time.time())) data = timestamp + body signature = hmac.new( hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256, ).hexdigest() return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', } # Usage body = json.dumps({"shop_domain": "cool-store.myshopify.com"}) headers = sign_request(body, os.environ["APPSTLE_HMAC_KEY"]) ``` ```bash curl theme={null} # Compute signature: HMAC-SHA256(timestamp + body, key) TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Which should I choose?** * **Partner Secret** — simpler to implement, fine for most integrations * **HMAC-SHA256** — better security (secret never sent over the wire), recommended for high-volume or security-sensitive integrations Both are equally supported. You can switch modes later by contacting Appstle. ### Step 1c: Choose your connect mode Appstle supports two ways to establish merchant connections. Your connect mode is configured during onboarding. #### Option A: Nonce Handshake (default) The full two-way verification flow described in this guide. Both sides verify each other using a one-time nonce. After the handshake, your app receives an Appstle API key (`apst_...`) to call Appstle's External API. **Best for:** partners who want to read/write data in Appstle (membership contracts, plans, billing, etc.) #### Option B: Simple Token Exchange A streamlined flow where your app sends its own access token to Appstle (or Appstle calls your connect endpoint and you return one). No nonce, no verify endpoint needed. Appstle stores your token and uses it to call your API when needed. **Best for:** partners where Appstle needs to call the partner's API (e.g., syncing data to the partner's platform), rather than the partner calling Appstle's API. **Key difference from Nonce Handshake:** in Simple Token Exchange, your app provides its own access token to Appstle. Appstle stores this token and uses it to push data to your API (via your `sync_path` — see [Data Sync](#data-sync-push-model) below). Your app does not receive an Appstle API key in this mode. **Need both directions?** If you need to both push data to Appstle AND have Appstle push data to you, use the Nonce Handshake mode and provide a `sync_path` during onboarding. Contact [support@appstle.com](mailto:support@appstle.com) to discuss your use case. **How Simple Token Exchange works.** *Partner-initiated:* ```bash theme={null} curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: 1709856000" \ -H "X-Partner-Signature: 5a3c1f2e..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "access_token": "your-apps-token-for-this-merchant" }' ``` Response: ```json theme={null} { "status": "pending_merchant_approval" } ``` Your `access_token` is stored securely but will not be activated until the merchant approves the connection from their Appstle dashboard. Once approved, Appstle calls your `/appstle/approved` endpoint to confirm (see [Handling the approval callback](#handling-the-approval-callback)). *Appstle-initiated:* Appstle calls your `/appstle/connect` endpoint with `{ "shop_domain": "..." }`. Your app responds with: ```json theme={null} { "success": true, "access_token": "your-apps-token-for-this-merchant" } ``` With Simple Token Exchange, your app does NOT receive an Appstle API key. If you also need to call Appstle's External API, use the Nonce Handshake mode instead. ### Step 2: Understand the callback nonce This section applies to Nonce Handshake mode only. If you're using Simple Token Exchange, skip to [Step 4](#step-4-implement-the-connect-flow-your-dashboard). Before implementing, you need to understand the callback nonce — it's the core security mechanism of the handshake. #### What is a callback nonce? A nonce (number used once) is a random, single-use string that proves both sides of the connection are who they claim to be. It prevents replay attacks and ensures the handshake can't be forged. #### Requirements | Requirement | Detail | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Length** | At least 32 bytes (64 hex characters) | | **Randomness** | Must be cryptographically random — do NOT use `Math.random()`, `rand()`, timestamps, or UUIDs | | **Single-use** | Each nonce must be used exactly once, then deleted | | **Expiry** | Nonces expire after 5 minutes on Appstle's side. Your storage should also expire them. | | **Storage** | Store temporarily with a TTL. Redis, DynamoDB, or any key-value store with expiry works. Database with a cleanup job is also fine. | #### How to generate a nonce Use your language's cryptographically secure random number generator. ```javascript Node.js theme={null} const crypto = require('crypto'); // Generate a 32-byte (64 hex character) cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```python Python theme={null} import secrets # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = secrets.token_hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```ruby Ruby theme={null} require 'securerandom' # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = SecureRandom.hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```php PHP theme={null} // Generate a 32-byte (64 hex character) cryptographically random nonce $nonce = bin2hex(random_bytes(32)); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```java Java theme={null} import java.security.SecureRandom; SecureRandom secureRandom = new SecureRandom(); byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); StringBuilder sb = new StringBuilder(64); for (byte b : bytes) { sb.append(String.format("%02x", b)); } String nonce = sb.toString(); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```go Go theme={null} import ( "crypto/rand" "encoding/hex" ) bytes := make([]byte, 32) rand.Read(bytes) nonce := hex.EncodeToString(bytes) // Result: "a1b2c3d4e5f6...64 characters total" ``` **Common mistakes:** * `Math.random().toString(36)` — not cryptographically random, predictable * `uuid.v4()` — UUIDs are not designed as security tokens (some implementations use weak RNG) * `Date.now().toString()` — trivially guessable * Reusing nonces across multiple connect attempts Always use your language's `crypto` / `secrets` / `SecureRandom` module. #### How to store a nonce Store the nonce temporarily, keyed by shop domain, with a 5-minute expiry. Delete it after verification. ```javascript Node.js + Redis theme={null} const Redis = require('ioredis'); const redis = new Redis(); // Store nonce with 5-minute TTL async function storeNonce(shopDomain, nonce) { const key = `appstle:nonce:${shopDomain}`; await redis.set(key, nonce, 'EX', 300); // 300 seconds = 5 minutes } // Retrieve and delete nonce (single atomic operation) async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const storedNonce = await redis.get(key); if (!storedNonce || storedNonce !== nonceToCheck) { return false; } await redis.del(key); return true; } ``` ```python Python + database theme={null} from datetime import datetime, timedelta from your_app.models import PartnerNonce # your ORM model def store_nonce(shop_domain: str, nonce: str): # Delete any existing nonce for this shop (prevent duplicates) PartnerNonce.objects.filter(shop_domain=shop_domain).delete() PartnerNonce.objects.create( shop_domain=shop_domain, nonce=nonce, expires_at=datetime.utcnow() + timedelta(minutes=5), ) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: try: record = PartnerNonce.objects.get( shop_domain=shop_domain, nonce=nonce_to_check, expires_at__gt=datetime.utcnow(), # not expired ) record.delete() return True except PartnerNonce.DoesNotExist: return False ``` ### Step 3: Implement your endpoints Your app must expose two HTTP endpoints that Appstle calls during the connection handshake. The paths default to `/appstle/connect` and `/appstle/verify` but can be customized during onboarding. **Both endpoints must:** * Accept `POST` requests with a JSON body * Return JSON responses * Be accessible over HTTPS (Appstle will not call HTTP endpoints) * Respond within 10 seconds (or the request will time out) * **Be idempotent.** Appstle may retry a callback on transient failure, and a merchant flipping connect/disconnect repeatedly will exercise the same endpoint with the same `(shop_domain, partnerId)` pair. Treat every call as an upsert keyed by `(shop_domain, partnerId)` — never blindly insert. The same rule applies to your `/appstle/approved` and `/appstle/disconnect` endpoints described later. #### Endpoint 1: `POST /appstle/connect` **When is this called?** Appstle calls this when a merchant initiates the connection from Appstle's dashboard (Flow B). **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "app": "memberships", "callback_url": "https://membership-admin.appstle.com/api/partner/your-partner-id/verify", "callback_nonce": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" } ``` | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `shop_domain` | string | The merchant's Shopify domain (e.g. `cool-store.myshopify.com`) | | `app` | string | Always `"memberships"` — identifies which Appstle app is connecting | | `callback_url` | string | The exact URL your app must call to complete the handshake. **The `{partnerId}` embedded in this URL is *Appstle's* identifier for this Appstle app on your side, not your Partner ID.** Use the URL verbatim — don't parse or substitute the segment. | | `callback_nonce` | string | A one-time-use token generated by Appstle. Expires in 5 minutes. | **What should your app do?** 1. **Validate the shop** — check that this `shop_domain` exists in your system. If you don't recognize the shop, return an error. 2. **Store the nonce and callback URL** — save `callback_nonce` and `callback_url` associated with this `shop_domain`. You'll need them to complete the handshake. 3. **Call back to Appstle** — either immediately (auto-approve) or after merchant confirmation, call the `callback_url` to complete the connection. See [Completing the handshake](#completing-the-handshake-flow-b) below. 4. **Return a success response** — any `2xx` status code tells Appstle the request was received. ```javascript Node.js (Express) theme={null} const express = require('express'); const axios = require('axios'); const router = express.Router(); router.post('/appstle/connect', async (req, res) => { const { shop_domain, app, callback_url, callback_nonce } = req.body; // 1. Validate: does this shop exist in your system? const shop = await db.shops.findOne({ domain: shop_domain }); if (!shop) { return res.status(400).json({ error: 'Shop not found in our system' }); } // 2. Store the nonce and callback URL for this shop await db.pendingConnections.upsert({ shopDomain: shop_domain, callbackUrl: callback_url, callbackNonce: callback_nonce, createdAt: new Date(), expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes }); // 3. Option A: Auto-approve (call back immediately) try { const response = await axios.post(callback_url, { shop_domain: shop_domain, callback_nonce: callback_nonce, }, { headers: { 'X-Partner-Secret': process.env.APPSTLE_PARTNER_SECRET, 'Content-Type': 'application/json', }, }); if (response.data.verified && response.data.access_token) { // 4. Store the access token for this merchant await db.appstleTokens.upsert({ shopDomain: shop_domain, accessToken: response.data.access_token, connectedAt: new Date(), }); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } // 5. Return success to Appstle res.json({ success: true }); }); ``` #### Endpoint 2: `POST /appstle/verify` **When is this called?** Appstle calls this when a merchant initiates the connection from your app's dashboard (Flow A). Appstle is asking your app: "Did you actually send this nonce?" **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f6...the-nonce-you-generated" } ``` | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------- | | `shop_domain` | string | The merchant's Shopify domain | | `callback_nonce` | string | The nonce your app originally sent in the `/connect` call | **What should your app do?** 1. Look up the stored nonce for this `shop_domain` 2. Compare the `callback_nonce` from the request against your stored nonce 3. If they match: delete the stored nonce (it's single-use) and return `{ "verified": true }` 4. If they don't match: return `{ "verified": false }` ```javascript Node.js (Express) theme={null} router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; // 1. Look up the stored nonce for this shop const isValid = await verifyAndDeleteNonce(shop_domain, callback_nonce); // 2. Return the result res.json({ verified: isValid }); }); ``` ### Step 4: Implement the connect flow (your dashboard) Now build the merchant-facing "Connect Appstle Memberships" button in your app's dashboard. #### Partner-initiated connect (Flow A) — step by step This is the flow where the merchant clicks "Connect Appstle" in your dashboard. The merchant initiates the connection from inside your app's UI. Store it keyed by `shop_domain` with a 5-minute TTL. Send the `shop_domain`, the `callback_nonce`, and your Partner Secret. Appstle also confirms the shop has Appstle Memberships installed. Payload: the `shop_domain` and the same `callback_nonce`. Confirm it matches, delete it, return `{ "verified": true }`. The connection is pending — no access token has been issued yet. They open **Settings → Partner Connections** and click **Approve**. The token is POSTed to YOUR `/appstle/approved` endpoint. Show "Connected!" to the merchant. You're done. **Full implementation (Node.js):** ```javascript theme={null} const crypto = require('crypto'); const axios = require('axios'); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL; // https://membership-admin.appstle.com // Called when merchant clicks "Connect Appstle" in your dashboard async function connectToAppstle(shopDomain) { // Step 2: Generate a cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Store it so your /appstle/verify endpoint can look it up later await storeNonce(shopDomain, nonce); // see nonce storage examples above // Step 3: Call Appstle's partner connect endpoint const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json', }, } ); // Steps 4-6 happen automatically (Appstle calls your /appstle/verify) // Step 7: Appstle returns pending status — token is NOT delivered yet const { status } = response.data; if (status === 'pending_merchant_approval') { // The merchant needs to approve in their Appstle dashboard. // Once approved, Appstle will POST the token to your /appstle/approved endpoint. await markConnectionPending(shopDomain); return { pending: true }; } throw new Error('Connection failed'); } ``` **curl equivalent:** ```bash theme={null} curl -X POST "https://membership-admin.appstle.com/api/partner/search-pie/connect" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890" }' ``` **Success response:** ```json theme={null} { "status": "pending_merchant_approval" } ``` **What happens next?** The merchant will see a "Pending Request" in their Appstle dashboard under **Settings → Partner Connections**. (This menu appears automatically once a partner initiates a connection request — it is not visible before any partner has connected.) When they click "Approve," Appstle creates a scoped API key and delivers it to your `/appstle/approved` endpoint (see [Handling the approval callback](#handling-the-approval-callback) below). Pending requests expire after 30 days if not acted on. **Deep link to approval screen:** You can redirect the merchant directly to the approval screen to minimize friction: ``` https://admin.shopify.com/store/{shop-handle}/apps/appstle-memberships/settings/partner-connections ``` Replace `{shop-handle}` with the merchant's store handle (the part before `.myshopify.com`). This takes them straight to the pending connection for one-click approval. You can trigger this redirect in your UI immediately after receiving the `pending_merchant_approval` response. #### Appstle-initiated connect (Flow B) — step by step This is the flow where the merchant clicks "Connect" in Appstle's dashboard. The connection is initiated from inside Appstle, not your UI. Internal Appstle call — your app is not involved yet. Payload: `shop_domain`, `app`, `callback_url`, and `callback_nonce`. Persist them keyed by `shop_domain` for the verify step. Send `shop_domain`, `callback_nonce`, and your Partner Secret. The `access_token` is returned in the response body. Connection complete. ##### Completing the handshake (Flow B) After your `/appstle/connect` endpoint receives the nonce and callback URL, your app completes the connection by calling Appstle's verify endpoint: ```bash theme={null} curl -X POST "https://membership-admin.appstle.com/api/partner/search-pie/verify" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "the-nonce-appstle-sent-in-the-connect-call" }' ``` **Success response:** ```json theme={null} { "verified": true, "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Failed response (nonce expired or mismatched):** ```json theme={null} { "verified": false } ``` You must call the verify endpoint within 5 minutes of receiving the nonce. After that, the nonce expires and the merchant will need to try again. ## Using the API token After a successful connection, your app has an `access_token` (prefixed with `apst_`). For Appstle-initiated connections (Flow B), the token is returned immediately in the verify response. For partner-initiated connections (Flow A), the token is delivered asynchronously to your `/appstle/approved` endpoint after the merchant approves (see [Handling the approval callback](#handling-the-approval-callback) below). Use this token exactly like a merchant API key — pass it in the `X-API-Key` header: ```bash theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/subscription-contract-details?shop=cool-store.myshopify.com" \ -H "X-API-Key: apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" ``` ### Token properties | Property | Detail | | -------------- | ------------------------------------------------------------------------------------------ | | **Format** | Starts with `apst_` followed by 40 alphanumeric characters | | **Scope** | One token per merchant per partner | | **Permission** | `READ_ONLY` or `READ_WRITE` (set during partner onboarding) | | **Billing** | Partner tokens bypass the paid API plan — merchants are never billed for partner API usage | | **Revocation** | Revoked instantly when the merchant disconnects or uninstalls Appstle | | **Expiry** | Tokens do not expire on their own. They remain valid until explicitly revoked. | ### Available endpoints Partner tokens grant access to the same External API endpoints as merchant API keys: * **Membership contracts** — `GET /api/external/v2/subscription-contract-details` (list membership contracts, filter by customer, status, plan) * **Cancel membership** — `DELETE /api/external/v2/subscription-contracts/{id}` *(requires READ\_WRITE)* * **Update payment method** — `PUT /api/external/v2/subscription-contracts-update-payment-method` *(requires READ\_WRITE)* * **Apply discount** — `PUT /api/external/v2/subscription-contracts-apply-discount` *(requires READ\_WRITE)* * **Add discount** — `PUT /api/external/v2/subscription-contracts-add-discount` *(requires READ\_WRITE)* * **Remove discount** — `PUT /api/external/v2/subscription-contracts-remove-discount` *(requires READ\_WRITE)* * **Add line item** — `PUT /api/external/v2/subscription-contracts-add-line-item` *(requires READ\_WRITE)* See the full [Integration guide](/memberships/integration-guide) for complete endpoint documentation. ## Data sync (push model) Some integrations work best when Appstle pushes data to your app, rather than your app pulling from Appstle's API. For example, an analytics platform might need Appstle to push membership data so it can be tracked alongside other store metrics. ### How it works During onboarding, you can configure a `sync_path` on your server (e.g., `/appstle/sync`). When membership events occur (memberships created, cancelled, billing attempts, plan changes, upgrades/downgrades), Appstle calls your endpoint with the relevant data. | Config field | Example | Description | | ----------------- | --------------------- | -------------------------------------------------- | | `sync_path` | `/appstle/sync` | Your endpoint where Appstle pushes membership data | | `disconnect_path` | `/appstle/disconnect` | Your endpoint called when a merchant disconnects | ### Authentication When Appstle calls your endpoints, it authenticates using the auth mode configured for your partner: * **Partner Secret mode:** no additional headers (your endpoints are responsible for validating the source — consider IP allowlisting) * **HMAC-SHA256 mode (recommended):** Appstle signs every request with `X-Partner-Timestamp` and `X-Partner-Signature` headers. Your app should verify the HMAC signature to confirm the request came from Appstle. **Verifying incoming HMAC signatures (Node.js):** ```javascript theme={null} const crypto = require('crypto'); function verifyAppstleSignature(req, hmacKey) { const timestamp = req.headers['x-partner-timestamp']; const signature = req.headers['x-partner-signature']; if (!timestamp || !signature) return false; // Reject if timestamp is more than 5 minutes old const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) return false; // Compute expected signature const data = timestamp + req.rawBody; // make sure you capture raw body const expected = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); // Constant-time comparison return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ### Pull vs push — which model do I need? | Model | Connect mode | Your app calls Appstle? | Appstle calls your app? | Use case | | ------------------ | ---------------------------- | ------------------------- | --------------------------------- | ------------------------------------------------- | | **Pull** (default) | Nonce Handshake | Yes (via `apst_` API key) | No | Helpdesks, CRMs reading membership data on demand | | **Push** | Simple Token Exchange | No | Yes (via your token + sync\_path) | Analytics tools, search platforms that index data | | **Bidirectional** | Nonce Handshake + sync\_path | Yes | Yes | Full two-way integrations | ## Disconnecting ### Merchant disconnects from Appstle Merchants can disconnect your integration anytime from **Appstle Dashboard → Settings → Partner Connections**. When they do: * Your API token for that merchant is revoked immediately * Subsequent API calls will return `401 Unauthorized` * Appstle sends a disconnect webhook to your app (if you configured a `disconnect_path` during onboarding — see below) * Your app should handle this gracefully and show a "Reconnect" option **Best practice:** in your API client, check for `401` responses and update your UI to show the connection as disconnected: ```javascript theme={null} async function callAppstleApi(shopDomain, endpoint) { const token = await getAccessToken(shopDomain); try { const response = await axios.get(`${APPSTLE_BASE}${endpoint}`, { headers: { 'X-API-Key': token }, }); return response.data; } catch (err) { if (err.response?.status === 401) { // Token was revoked — merchant disconnected await markAsDisconnected(shopDomain); throw new Error('Appstle connection was revoked. Merchant needs to reconnect.'); } throw err; } } ``` ### Disconnect webhook (first-class endpoint) Configure a `disconnect_path` during onboarding (defaults to `/appstle/disconnect`). Appstle calls this whenever a merchant disconnects — from the Appstle dashboard, from your app, or by uninstalling Appstle entirely. **You should implement this endpoint for every integration** — it is the only reliable signal that the merchant has revoked access on Appstle's side. Polling `401` responses as a fallback works but lags behind. **Request body from Appstle:** ```json theme={null} { "shop_domain": "cool-store.myshopify.com" } ``` If your partner uses HMAC-SHA256 auth, the webhook includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. **Your endpoint must:** 1. **Look up the connection without filtering on status.** Don't `WHERE status = 'active'` — if the merchant rapid-clicks disconnect twice, the second call may arrive when the row is already inactive. Find by `(shop_domain, partnerId)` only. 2. **Revoke the Appstle access token idempotently.** If the token is already revoked or absent, return success — don't error. Revocation must be safe to call repeatedly. 3. **Mark the local connection inactive.** Clear or null out the stored Appstle token so subsequent API calls don't try to use it. 4. **Return `2xx` even when there was nothing to do.** A no-op disconnect is a successful disconnect from Appstle's perspective. ```javascript Node.js (Express) theme={null} router.post('/appstle/disconnect', async (req, res) => { const { shop_domain } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } // 1. Status-agnostic lookup — don't filter on .where({ status: 'active' }) const connection = await db.connections.findOne({ shopDomain: shop_domain }); if (connection) { // 2. Idempotent token revoke — clearing a null token is a no-op await db.appstleTokens.delete({ shopDomain: shop_domain }); // 3. Mark inactive (upsert-style — safe if already inactive) await db.connections.update( { shopDomain: shop_domain }, { status: 'disconnected', disconnectedAt: new Date() } ); } // 4. Always 2xx — even if nothing was found res.json({ success: true }); }); ``` ```python Python (Flask) theme={null} @app.route("/appstle/disconnect", methods=["POST"]) def appstle_disconnect(): shop_domain = request.json["shop_domain"] # 1. Find without filtering on status connection = Connection.query.filter_by(shop_domain=shop_domain).first() if connection: # 2. Idempotent token revoke AppstleToken.query.filter_by(shop_domain=shop_domain).delete() # 3. Mark inactive (upsert semantics) connection.status = "disconnected" connection.disconnected_at = datetime.utcnow() db.session.commit() # 4. Always 2xx return jsonify({"success": True}) ``` This is a best-effort notification — your app should also handle `401` responses from the Appstle API as a fallback signal that the connection was revoked. ### Partner disconnects programmatically Your app can disconnect a merchant using your partner authentication (Partner Secret or HMAC-SHA256): ```bash With Partner Secret theme={null} curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Secret: YOUR_PARTNER_SECRET" \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com" }' ``` ```bash With HMAC-SHA256 theme={null} TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Response:** ```json theme={null} { "success": true } ``` ### Check connection status `GET /api/partner/{partnerId}/status?shop_domain=...` is the **authoritative source of truth** for whether a merchant is connected. If your UI shows a "Connected" badge, derive it from this endpoint — not from whether you happen to have a stored API key locally. **Why this matters:** Older integrations sometimes inferred "connected" from the presence of a per-app API key column in their own database. That column is now a deprecated fallback — it can be stale (key revoked on Appstle's side, your row never updated) and it can't represent `pending_merchant_approval` or `rejected`. Always call `/status` before showing connection state to the merchant or making business decisions based on it. Partners can authenticate with their Partner Secret or HMAC signature: ```bash theme={null} # With Partner Secret curl -X GET "https://membership-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Secret: your-partner-secret" # With HMAC curl -X GET "https://membership-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" ``` **Response (active connection):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "active", "connected_at": "2026-03-07T20:30:00Z" } ``` **Response (pending merchant approval):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "pending_merchant_approval" } ``` **All possible status values:** | Status | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | Connected and working — API token is valid | | `pending_merchant_approval` | Partner-initiated connect is awaiting merchant approval | | `rejected` | Merchant rejected the connection request | | `expired` | A pending request expired (30-day window) without merchant action — partner must initiate a new connection | | `not_connected` | No connection record exists for this partner + shop, or the connection was previously terminated (by merchant, by partner, or by uninstall). In both cases the token is revoked and your app would need to initiate a new connection. | Use the status endpoint to poll for approval if your app doesn't implement the `/appstle/approved` callback. Poll every 30–60 seconds after initiating a connect. Once the status changes from `pending_merchant_approval` to `active`, your token has been delivered via the approval callback (or you can request it again). ## Handling the approval callback When a merchant approves a partner-initiated connection, Appstle delivers the API token by calling an endpoint on your server. This applies to partner-initiated connections only — Appstle-initiated connections (Flow B) return the token immediately. ### Endpoint: `POST /appstle/approved` The path defaults to `/appstle/approved` but can be customized during onboarding (configured as `approval_callback_path`). Your `/appstle/approved` endpoint **must accept unauthenticated POST requests** from Appstle's servers. Do not put authentication middleware (e.g., JWT validation, API key checks) on this endpoint — Appstle will not send your app's auth credentials when calling this callback. If you need to verify the request is from Appstle, use [HMAC-SHA256 authentication mode](#option-b-hmac-sha256) — when enabled, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) you can verify. **Request body from Appstle (Nonce Handshake mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Request body from Appstle (Simple Token Exchange mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "status": "approved" } ``` In Simple Token Exchange mode, your app already provided its own token during the connect call. The approval callback simply confirms the connection is now active — Appstle will start using your token for API calls. **Expected response:** Return any `2xx` status code with a JSON body (e.g., `{ "success": true }`). If your endpoint returns a non-2xx status (e.g., `401 Unauthorized`), the connection is still approved on Appstle's side, but your app won't know — see [What if the callback fails?](#what-if-the-callback-fails) below. If your partner uses HMAC-SHA256 auth, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. ```javascript Node.js theme={null} router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } if (access_token) { // Nonce Handshake mode — store the Appstle API token await saveToken(shop_domain, access_token); console.log(`Connection approved for ${shop_domain} — token received`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active await markConnectionActive(shop_domain); console.log(`Connection approved for ${shop_domain} — our token is now active`); } res.json({ success: true }); }); ``` ```python Python theme={null} @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active mark_connection_active(shop_domain) return jsonify({"success": True}) ``` ### What if the callback fails? If your endpoint is unreachable or returns an error, the connection is still approved on Appstle's side. The API token exists and is valid. Your app can: 1. **Poll the status endpoint** — check `GET /api/partner/{id}/status?shop_domain=...` until the status is `active` 2. **Retry from Appstle's side** — currently, Appstle does not automatically retry the callback. Contact support if you need the token re-delivered. ### What if the merchant rejects? If the merchant clicks "Reject," the connection status changes to `rejected` and Appstle notifies your app via the disconnect webhook (if configured). Your app should handle this gracefully — show the merchant that the connection was not approved. ## Error handling All partner endpoints return structured error responses: ```json theme={null} { "type": "https://membership-admin.appstle.com/problem", "title": "Bad Request", "status": 400, "detail": "UserGeneratedError:Active connection already exists. Disconnect first.", "errorKey": "ALREADY_CONNECTED" } ``` ### Error codes | Error code | HTTP status | When it happens | What to do | | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PARTNER_NOT_FOUND` | 400 | Your Partner ID is wrong, or the partner has been deactivated | Double-check your Partner ID. Contact Appstle if unexpected. | | `TOKEN_INVALID` | 400 | Auth failed: `X-Partner-Secret` is wrong, or HMAC signature is invalid, or timestamp is >5 min off | Verify your secret or HMAC key. Check for trailing whitespace. For HMAC: ensure server clock is synced (NTP) and you're signing `timestamp + body` exactly. | | `SHOP_NOT_FOUND` | 400 | The shop doesn't have Appstle Memberships installed | Tell the merchant to install Appstle Memberships first. | | `ALREADY_CONNECTED` | 400 | An active connection already exists for this partner + shop | Call disconnect first, then reconnect. Or skip — you're already connected. | | `VERIFICATION_FAILED` | 400 | Nonce didn't match, expired (>5 min), or your `/verify` endpoint returned `false` | Generate a fresh nonce and try again. Check your nonce storage logic. | | `NOT_CONNECTED` | 400 | Trying to disconnect or check status, but no active connection exists | The merchant may have already disconnected from their side. | | `PARTNER_UNREACHABLE` | 400 | Appstle couldn't reach your `/appstle/connect` or `/appstle/verify` endpoint | Check your endpoint URL is correct, HTTPS, and publicly accessible. Check your server logs. | | `UNEXPECTED_ROLLBACK` | 500 | Your endpoint returned success, but Appstle's transaction was silently rolled back. Manifests in logs as `UnexpectedRollbackException` / `Transaction silently rolled back because it has been marked as rollback-only`. | Common footgun for partners running on transactional frameworks: an inner write throws and gets caught by your handler, but the surrounding transaction has already been marked rollback-only — so the outer commit fails with no visible error from your business logic. Fix is in your code: either let the inner exception propagate, or perform the write in a fresh inner transaction. Don't swallow exceptions inside a transactional boundary. | #### Idempotency requirements — recap The integration framework relies on partners treating callbacks as **at-least-once**. Concretely: * **Connect / approval callbacks:** upsert by `(shop_domain, partnerId)`. Two `/appstle/approved` calls for the same shop must produce the same end state, not two rows. * **Disconnect callback:** find the connection without filtering on status; revoke tokens idempotently; return `2xx` even when there is nothing to do. * **Status reads:** safe by definition — no side effects. If your code is built on "this only ever fires once", expect bugs the first time the merchant flips connect/disconnect quickly or the first time a network blip triggers an Appstle retry. ## Security checklist Before going live, verify all of these: * Partner Secret / HMAC key is stored in environment variables or a secrets manager — not hardcoded in source code * Nonces are generated using a cryptographically secure random generator (`crypto.randomBytes`, `secrets.token_hex`, `SecureRandom`, etc.) * Nonces are stored with a TTL (≤ 5 minutes) and deleted after verification * Nonces are compared using a constant-time comparison to prevent timing attacks (most frameworks do this by default for string equality) * Endpoints are served over HTTPS — Appstle will not call HTTP endpoints * `shop_domain` is validated in your `/appstle/connect` and `/appstle/verify` endpoints — reject domains you don't recognize * Access tokens are stored encrypted at rest (or in a secrets manager) * 401 responses are handled gracefully — show a "Reconnect" option, don't break silently * Error responses from Appstle are logged for debugging * (HMAC only) Server clock is synced via NTP — timestamps more than 5 minutes off will be rejected * (If using disconnect webhook) Your `/appstle/disconnect` endpoint cleans up stored tokens and marks the connection as inactive ## Complete example: partner-initiated flow (Node.js) Here's a full, copy-pasteable implementation of Flow A in Express: ```javascript theme={null} // appstle-partner.js const express = require('express'); const crypto = require('crypto'); const axios = require('axios'); const Redis = require('ioredis'); const router = express.Router(); const redis = new Redis(process.env.REDIS_URL); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL || 'https://membership-admin.appstle.com'; const NONCE_TTL = 300; // 5 minutes in seconds // ────────────────────────────────────────────── // Nonce helpers // ────────────────────────────────────────────── async function storeNonce(shopDomain, nonce) { await redis.set(`appstle:nonce:${shopDomain}`, nonce, 'EX', NONCE_TTL); } async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const stored = await redis.get(key); if (!stored || stored !== nonceToCheck) return false; await redis.del(key); return true; } // ────────────────────────────────────────────── // Token storage (use your database in production) // ────────────────────────────────────────────── async function saveToken(shopDomain, accessToken) { // In production: encrypt the token before storing await redis.set(`appstle:token:${shopDomain}`, accessToken); } async function getToken(shopDomain) { return redis.get(`appstle:token:${shopDomain}`); } // ────────────────────────────────────────────── // Flow A: Partner-initiated connect // Called when merchant clicks "Connect Appstle" in YOUR dashboard // ────────────────────────────────────────────── router.post('/connect-appstle', async (req, res) => { const { shopDomain } = req.body; try { // 1. Generate nonce const nonce = crypto.randomBytes(32).toString('hex'); await storeNonce(shopDomain, nonce); // 2. Call Appstle const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' } } ); // 3. Connection is now pending merchant approval if (response.data.status === 'pending_merchant_approval') { // Mark as pending in your system — show the merchant a "waiting for approval" state await redis.set(`appstle:pending:${shopDomain}`, 'true'); return res.json({ pending: true, message: 'Waiting for merchant to approve in Appstle dashboard' }); } res.status(400).json({ error: 'Connection failed' }); } catch (err) { const detail = err.response?.data?.detail || err.message; console.error('Appstle connect failed:', detail); res.status(400).json({ error: detail }); } }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/verify // Called BY Appstle during Flow A to verify your nonce // ────────────────────────────────────────────── router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; const verified = await verifyAndDeleteNonce(shop_domain, callback_nonce); res.json({ verified }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/approved // Called BY Appstle when merchant approves a partner-initiated connection // ────────────────────────────────────────────── router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; if (access_token) { // Nonce Handshake mode — Appstle is delivering our API token await saveToken(shop_domain, access_token); await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Token received for ${shop_domain}`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active on Appstle's side await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Our token is now active for ${shop_domain}`); } res.json({ success: true }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/connect // Called BY Appstle during Flow B (Appstle-initiated) // ────────────────────────────────────────────── router.post('/appstle/connect', async (req, res) => { const { shop_domain, callback_url, callback_nonce } = req.body; // Verify the shop exists in your system // const shop = await db.shops.findOne({ domain: shop_domain }); // if (!shop) return res.status(400).json({ error: 'Unknown shop' }); // Auto-approve: immediately call back to complete the handshake // (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try { const response = await axios.post(callback_url, { shop_domain, callback_nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' }, }); if (response.data.verified && response.data.access_token) { await saveToken(shop_domain, response.data.access_token); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } res.json({ success: true }); }); module.exports = router; ``` ## Complete example: partner-initiated flow (Python) ```python theme={null} # appstle_partner.py import os import secrets import redis import requests from flask import Flask, request, jsonify app = Flask(__name__) r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379")) PARTNER_ID = os.environ["APPSTLE_PARTNER_ID"] PARTNER_SECRET = os.environ["APPSTLE_PARTNER_SECRET"] APPSTLE_BASE = os.environ.get("APPSTLE_BASE_URL", "https://membership-admin.appstle.com") NONCE_TTL = 300 # 5 minutes # ── Nonce helpers ── def store_nonce(shop_domain: str, nonce: str): r.set(f"appstle:nonce:{shop_domain}", nonce, ex=NONCE_TTL) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: key = f"appstle:nonce:{shop_domain}" stored = r.get(key) if not stored or stored.decode() != nonce_to_check: return False r.delete(key) return True # ── Token storage ── def save_token(shop_domain: str, access_token: str): r.set(f"appstle:token:{shop_domain}", access_token) def get_token(shop_domain: str): val = r.get(f"appstle:token:{shop_domain}") return val.decode() if val else None # ── Flow A: Partner-initiated connect ── @app.route("/connect-appstle", methods=["POST"]) def connect_appstle(): shop_domain = request.json["shopDomain"] # 1. Generate nonce nonce = secrets.token_hex(32) store_nonce(shop_domain, nonce) # 2. Call Appstle resp = requests.post( f"{APPSTLE_BASE}/api/partner/{PARTNER_ID}/connect", json={"shop_domain": shop_domain, "callback_nonce": nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) resp.raise_for_status() data = resp.json() # 3. Connection is pending merchant approval if data.get("status") == "pending_merchant_approval": r.set(f"appstle:pending:{shop_domain}", "true") return jsonify({"pending": True, "message": "Waiting for merchant to approve in Appstle dashboard"}) return jsonify({"error": "Connection failed"}), 400 # ── Endpoint: POST /appstle/verify (called BY Appstle during Flow A) ── @app.route("/appstle/verify", methods=["POST"]) def appstle_verify(): body = request.json verified = verify_and_delete_nonce(body["shop_domain"], body["callback_nonce"]) return jsonify({"verified": verified}) # ── Endpoint: POST /appstle/approved (called BY Appstle when merchant approves) ── @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active pass # mark connection as active in your DB r.delete(f"appstle:pending:{shop_domain}") return jsonify({"success": True}) # ── Endpoint: POST /appstle/connect (called BY Appstle during Flow B) ── @app.route("/appstle/connect", methods=["POST"]) def appstle_connect(): body = request.json shop_domain = body["shop_domain"] callback_url = body["callback_url"] callback_nonce = body["callback_nonce"] # Auto-approve: call back immediately # (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try: resp = requests.post( callback_url, json={"shop_domain": shop_domain, "callback_nonce": callback_nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) data = resp.json() if data.get("verified") and data.get("access_token"): save_token(shop_domain, data["access_token"]) except Exception as e: app.logger.error(f"Handshake failed: {e}") return jsonify({"success": True}) ``` ## FAQ Yes. Each partner gets its own scoped API token. Merchants can connect as many partners as they want. The tokens are completely independent. All active partner connections for that shop are automatically disconnected. Your tokens will stop working (401 responses). Yes — contact the Appstle team. New connections will use the updated permission, but existing connections keep their original permission until reconnected. Partner tokens share the same rate limits as regular API keys. If you receive a `429 Too Many Requests`, implement exponential backoff. Use a Shopify development store with Appstle Memberships installed. The partner integration works identically in development and production. You can use a tool like [ngrok](https://ngrok.com) to expose your local endpoints to the internet for testing. The merchant simply needs to click "Connect" again. A new nonce will be generated. Old nonces are automatically cleaned up. Yes. Flow A is for when the merchant connects from your dashboard. Flow B is for when they connect from Appstle's dashboard. Both are needed for a complete integration. You also need the `/appstle/approved` endpoint to receive API tokens after merchant approval (Flow A). No. A new connect handshake requires the merchant to initiate it from one of the dashboards. This is by design — merchants must explicitly authorize each connection. For security and trust. When your app initiates a connection, the merchant hasn't explicitly agreed on Appstle's side. The approval step ensures merchants consciously grant API access to partner apps. Appstle-initiated connections (Flow B) skip this step because the merchant is already clicking "Connect" in the Appstle dashboard. Pending connection requests expire after 30 days. If the merchant doesn't approve or reject within that window, the request expires and your app will need to initiate a new connection. The status changes to `rejected` and your app is notified via the disconnect webhook (if configured). The merchant can be asked to reconnect later if they change their mind — your app can initiate a new connection request. ## Need help? * **Partner onboarding & technical support:** [support@appstle.com](mailto:support@appstle.com) * **Integration guide:** [Third-Party Integration Guide](/memberships/integration-guide) (for direct API key usage) # Get started with Appstle Memberships API Source: https://developers.appstle.com/memberships/quickstart Go from zero to your first Memberships API response in minutes: create an API key, retrieve a customer's membership contracts, and check active status. This guide walks you through the three steps you need to make your first successful Appstle Memberships API call: get an API key, retrieve membership contracts, and verify a customer's membership status. ## Before you begin You need an active Appstle Memberships installation on your Shopify store. If you haven't installed the app yet, start in the Shopify App Store before continuing. ## Step 1 — Create an API key In your Appstle admin panel, go to **Settings → API Key Management**. Click **Create New Key**. Give it a name like `Quickstart Test` so you can identify it later. Your key is shown only once. Copy it now — you cannot retrieve the full value again after leaving this screen. Your key will look like this: ``` apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012 ``` Never use your API key in client-side code or commit it to a public repository. Store it in an environment variable and read it at runtime. ## Step 2 — Retrieve membership contracts Use your new key to fetch all membership contracts for a customer. Replace `your-store.myshopify.com` with your store's Shopify domain and `12345` with a real Shopify customer ID. ```bash cURL theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-contracts?shop=your-store.myshopify.com&customerId=12345" \ -H "X-API-Key: apst_your-api-key-here" ``` ```javascript Node.js theme={null} const res = await fetch( 'https://membership-admin.appstle.com/api/external/v2/membership-contracts' + '?shop=your-store.myshopify.com&customerId=12345', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY }, } ); const data = await res.json(); console.log(data); ``` ```python Python theme={null} import requests, os r = requests.get( 'https://membership-admin.appstle.com/api/external/v2/membership-contracts', params={'shop': 'your-store.myshopify.com', 'customerId': '12345'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) print(r.json()) ``` A successful response looks like this: ```json theme={null} { "content": [ { "id": 1001, "status": "ACTIVE", "membershipPlanName": "Gold Member", "nextBillingDate": "2026-03-01", "billingCycleType": "MONTHLY", "startDate": "2026-01-01", "endDate": null } ], "totalElements": 1 } ``` The `content` array contains each membership contract for the customer. `totalElements` tells you how many contracts exist in total (useful for pagination). If `content` is an empty array, the customer either has no memberships or no active ones. Try with a customer ID that you know has an active membership in your store. ## Step 3 — Check member status To quickly verify whether a customer is an active member — for example, before granting access to gated content — call the membership status endpoint: ```bash cURL theme={null} curl -X GET \ "https://membership-admin.appstle.com/api/external/v2/membership-status?shop=your-store.myshopify.com&customerId=12345" \ -H "X-API-Key: apst_your-api-key-here" ``` ```javascript Node.js theme={null} const res = await fetch( 'https://membership-admin.appstle.com/api/external/v2/membership-status' + '?shop=your-store.myshopify.com&customerId=12345', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY }, } ); const status = await res.json(); ``` ```python Python theme={null} r = requests.get( 'https://membership-admin.appstle.com/api/external/v2/membership-status', params={'shop': 'your-store.myshopify.com', 'customerId': '12345'}, headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) ``` In your integration, check the contract `status` field from the contracts endpoint: ```javascript theme={null} const isActiveMember = data.content.some(contract => contract.status === 'ACTIVE'); if (isActiveMember) { // Grant access to gated content } else { // Redirect to membership plan selection page } ``` ## Troubleshooting Your API key is invalid, missing, or was revoked. Double-check the `X-API-Key` header value. Make sure there is no extra whitespace and that the key belongs to the correct store. A required query parameter is missing or malformed. Confirm that `shop` is your full `.myshopify.com` domain and that `customerId` is a numeric Shopify customer ID. The customer exists but has no memberships. Use a different customer ID, or log in to your Appstle dashboard and confirm which customers have active contracts. You have exceeded the rate limit for your store. Implement exponential backoff and retry after a short delay. ## Next steps Full patterns for CRMs, email platforms, access control, and analytics integrations. Receive real-time events when memberships are created, cancelled, or renewed. # Automate memberships with Shopify Flow Source: https://developers.appstle.com/memberships/shopify-flow Automate membership workflows with Appstle's Shopify Flow triggers: tag customers, send emails, and update CRMs on lifecycle and billing events. Appstle Memberships integrates natively with Shopify Flow, Shopify's built-in automation platform. You can automate membership operations — send emails, tag customers, update CRMs, trigger Slack alerts, and more — without writing any code. Flow actions run within Shopify's authenticated context, so there is no need for API tokens or credentials. This makes Flow the most secure and straightforward way to automate membership operations. ## Getting started In your Appstle Memberships admin, go to **Settings → Integrations → Shopify Flow** and enable it. Navigate to the [Shopify Flow editor](https://admin.shopify.com/store/YOUR_STORE/apps/flow) in your Shopify admin. Click **Create workflow**, then select an Appstle Memberships trigger to start. All membership triggers appear under the Appstle Memberships app in the trigger picker. Shopify Flow is available on Shopify Basic plan and above. All Flow trigger events are logged in the membership's Activity Log with source `SYSTEM_EVENT`. ## Membership lifecycle triggers These triggers fire automatically when membership contracts change state. All lifecycle triggers include a full set of membership and customer properties. | Trigger | Handle | Description | | ------------------------ | -------------------------- | --------------------------------------------------------------------------------------- | | Membership Created | `membership-created` | A new membership contract is created. Includes the `order_id` of the originating order. | | Membership Updated | `membership-updated` | Membership details are modified, such as address or custom attributes. | | Membership Activated | `membership-activated` | A paused or new membership becomes active. | | Membership Paused | `membership-paused` | Membership is put on pause. | | Membership Cancelled | `membership-cancelled` | Membership is cancelled. | | Membership Expired | `membership-expired` | Membership expires, or perks are removed for cancelled/paused memberships. | | Membership Swap Product | `membership-swap-product` | The product or plan in a membership is changed. | | Next Order Date Changed | `next-order-date-changed` | The next renewal date is rescheduled. | | Billing Interval Changed | `billing-interval-changed` | Billing frequency is updated, for example monthly to annual. | ## Billing triggers | Trigger | Handle | Description | | -------------------------- | ---------------------------- | -------------------------------------------------------------------------- | | Membership Billing Success | `membership-billing-success` | Renewal payment processed successfully. Includes the new order ID. | | Membership Billing Failure | `membership-billing-failure` | Renewal payment failed. Use this to trigger dunning emails or pause logic. | The `membership-billing-failure` trigger fires on every failed attempt — not just the first one. Use the `Billing Attempt Count` property to distinguish first failures from retries so you don't send duplicate emails to the same customer. ## Trigger properties All triggers include the following properties: ### Membership properties | Property | Type | Description | | ------------------------------ | -------- | ----------------------------------------------------------------------------- | | `Membership ID` | Number | Internal membership contract ID | | `Status` | String | Current status: `ACTIVE`, `PAUSED`, `CANCELLED`, `EXPIRED`, `FAILED` | | `Next Billing Date` | DateTime | ISO 8601 next renewal date | | `Billing Interval` | String | Billing frequency unit: `DAY`, `WEEK`, `MONTH`, `YEAR` | | `Billing Interval Count` | Number | Number of intervals between renewals (e.g., `1` for monthly, `12` for annual) | | `Membership Selling Plan Name` | String | Name of the membership plan (e.g., "Gold Member — Monthly") | | `Membership Selling Plan ID` | Number | Shopify selling plan ID | | `product_id` | Number | Shopify product ID for the membership product | | `Variant ID` | Number | Shopify product variant ID | | `Original Order ID` | Number | Order ID that created the membership | | `Original Order Name` | String | Order name (e.g., "#1001") | | `Total Successful Orders` | Number | Total number of successfully completed orders for this membership contract | ### Customer properties | Property | Type | Description | | ----------------------- | ------ | ---------------------------- | | `customer_id` | Number | Shopify customer ID | | `Customer Email` | String | Customer's email address | | `Customer Phone` | String | Customer's phone number | | `Customer Display Name` | String | Customer's full display name | | `Customer First Name` | String | Customer's first name | | `Customer Last Name` | String | Customer's last name | ### Billing event properties The **Membership Billing Success** and **Membership Billing Failure** triggers include these additional fields: | Property | Type | Description | | ------------------------ | -------- | -------------------------------------------------------- | | `Billing Attempt Status` | String | `SUCCESS` or `FAILURE` | | `Billing Attempt ID` | Number | Shopify billing attempt ID | | `Billing Date` | DateTime | Scheduled billing date (ISO 8601) | | `Billing Attempt Time` | DateTime | Actual attempt timestamp (ISO 8601) | | `Billing Attempt Count` | Number | How many times billing has been attempted for this cycle | The **Membership Billing Success** trigger also includes: | Property | Type | Description | | ---------------------- | ------ | -------------------------------------------------- | | `order_id` | Number | Shopify order ID created by the successful renewal | | `Recurring Order ID` | Number | Same as `order_id` | | `Recurring Order Name` | String | Order name (e.g., "#1002") | ## Example workflows **Trigger:** Membership Created **Action:** Klaviyo / Omnisend / Shopify Email — send "Welcome to the membership" email Use `Customer Email` as the recipient and `Membership Selling Plan Name` to personalize the plan name in the email body. **Trigger:** Membership Updated **Condition:** `Membership Selling Plan Name` contains `"Gold"` **Action:** Shopify — Add customer tag `gold-member` This pattern lets you apply tier-specific tags automatically as members upgrade or switch plans. **Trigger:** Membership Billing Failure **Condition:** `Billing Attempt Count` equals `1` (first failure only) **Action:** Klaviyo — Trigger "Update Payment Method" flow Pass `Customer Email` and `Membership ID` as variables to the email template. **Trigger:** Membership Cancelled **Action 1:** Shopify — Add customer tag `membership-cancelled` **Action 2:** Klaviyo — Send "We're sorry to see you go" email with a win-back offer **Trigger:** Membership Billing Success **Action:** Salesforce / HubSpot (via Flow connector) — Log renewal event Use `order_id`, `Customer Email`, and `Membership Selling Plan Name` as the data payload. **Trigger:** Membership Expired **Action:** Slack — Send message to `#membership-churn` channel Include `Customer Display Name`, `Customer Email`, and `Membership Selling Plan Name` in the message body. ## Notes * Imported membership contracts do not fire `membership-created`. Only contracts created through the Shopify checkout flow trigger this event. * Flow integration must be enabled in **Settings → Integrations** before any triggers will fire. * All Flow trigger events appear in the membership Activity Log with source `SYSTEM_EVENT`. # Appstle Memberships webhook events and setup Source: https://developers.appstle.com/memberships/webhooks Receive real-time membership lifecycle and billing event notifications via webhooks. Covers Svix signature verification, automatic retries, and idempotency. Appstle Memberships webhooks deliver real-time HTTP notifications to your server whenever a membership event occurs. Webhooks are powered by [Svix](https://www.svix.com/) — enterprise-grade infrastructure with automatic retries, cryptographic signature verification, and detailed delivery monitoring. ## Getting started In your Appstle Memberships admin, go to **Settings → Webhooks**. Click **Add Endpoint** and enter your HTTPS endpoint URL. Choose which events you want to receive, or subscribe to all events to ensure you never miss one. Your endpoint will start receiving events immediately after saving. Webhooks are available on paid plans. Contact [support@appstle.com](mailto:support@appstle.com) to enable access. ## How webhooks work Webhooks are HTTP `POST` requests sent to your endpoint whenever a membership event occurs. Your endpoint must: * Be publicly accessible via HTTPS * Return a `2xx` status code within the timeout window * Process events asynchronously — queue them for background processing, then respond immediately Svix provides automatic retries with exponential backoff, cryptographic signature verification, detailed delivery logs, and the ability to replay events from your dashboard. ## Event types ### Membership lifecycle events | Event | Description | | ------------------------------------- | ------------------------------------- | | `membership.created` | New membership contract created | | `membership.updated` | Membership details modified | | `membership.activated` | Membership activated | | `membership.paused` | Membership paused | | `membership.cancelled` | Membership cancelled | | `membership.expired` | Membership expired at end of term | | `membership.swap-product` | Product or plan in membership changed | | `membership.next-order-date-changed` | Next renewal date rescheduled | | `membership.billing-interval-changed` | Billing frequency changed | ### Billing events | Event | Description | | ---------------------------- | -------------------------------------- | | `membership.billing-success` | Renewal payment processed successfully | | `membership.billing-failure` | Renewal payment failed | ## Payload structure All webhooks share this top-level structure: ```json theme={null} { "type": "membership.created", "data": {} } ``` The `data` field contains the event-specific payload. ### Membership contract payload The lifecycle events `membership.created`, `membership.updated`, `membership.activated`, `membership.paused`, `membership.cancelled`, `membership.expired`, `membership.swap-product`, `membership.next-order-date-changed`, and `membership.billing-interval-changed` all deliver a full membership contract object: ```json theme={null} { "type": "membership.created", "data": { "id": "gid://shopify/SubscriptionContract/12345", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z", "nextBillingDate": "2026-02-15", "status": "ACTIVE", "billingPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [], "maxCycles": 12, "minCycles": 1 }, "deliveryPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [] }, "lines": { "nodes": [ { "id": "gid://shopify/SubscriptionLine/67890", "productId": "gid://shopify/Product/11111", "variantId": "gid://shopify/ProductVariant/22222", "sellingPlanId": "gid://shopify/SellingPlan/33333", "sellingPlanName": "Gold Member — Monthly", "title": "Gold Membership", "variantTitle": "Monthly", "quantity": 1, "currentPrice": { "amount": "29.00", "currencyCode": "USD" } } ] }, "customer": { "id": "gid://shopify/Customer/55555", "email": "member@example.com", "displayName": "Jane Doe", "firstName": "Jane", "lastName": "Doe", "phone": "+1-555-123-4567" }, "originOrder": { "id": "gid://shopify/Order/77777", "name": "#1001" }, "deliveryPrice": { "amount": "0.00", "currencyCode": "USD" }, "lastPaymentStatus": "SUCCEEDED", "note": null, "customAttributes": [] } } ``` ### Billing success payload ```json theme={null} { "type": "membership.billing-success", "data": { "id": 98765, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99999", "contractId": 12345, "status": "SUCCESS", "billingDate": "2026-02-15T00:00:00Z", "attemptTime": "2026-02-15T10:30:00Z", "attemptCount": 1, "orderId": 77778, "orderName": "#1002", "orderAmount": 29.00, "retryingNeeded": false, "billingAttemptResponseMessage": null } } ``` ### Billing failure payload ```json theme={null} { "type": "membership.billing-failure", "data": { "id": 98766, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99998", "contractId": 12345, "status": "FAILURE", "billingDate": "2026-03-15T00:00:00Z", "attemptTime": "2026-03-15T10:30:00Z", "attemptCount": 1, "orderId": null, "orderName": null, "orderAmount": null, "retryingNeeded": true, "billingAttemptResponseMessage": "INVALID_PAYMENT_METHOD: The payment method is invalid." } } ``` Common `billingAttemptResponseMessage` values: | Value | Meaning | | ------------------------- | -------------------------------------------------- | | `INVALID_PAYMENT_METHOD` | Payment method is expired or invalid | | `INSUFFICIENT_FUNDS` | Insufficient account balance | | `CARD_DECLINED` | Card declined by issuer | | `AUTHENTICATION_REQUIRED` | Customer must re-authenticate their payment method | | `EXPIRED_PAYMENT_METHOD` | Payment method has expired | ## Signature verification Every webhook request is signed by Svix. Always verify the signature before processing the payload — this ensures the request genuinely came from Appstle and has not been tampered with. Svix includes three headers on every request: | Header | Description | | ---------------- | --------------------------------------------- | | `svix-id` | Unique message ID — use as an idempotency key | | `svix-timestamp` | Unix timestamp of when the message was sent | | `svix-signature` | HMAC-SHA256 signature | Find your **webhook signing secret** in your Appstle dashboard under **Settings → Webhooks → \[your endpoint]**. ```javascript Node.js theme={null} const { Webhook } = require('svix'); const secret = 'whsec_your_signing_secret'; app.post('/webhooks/appstle-memberships', express.raw({ type: 'application/json' }), (req, res) => { const wh = new Webhook(secret); let event; try { event = wh.verify(req.body, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }); } catch (err) { return res.status(400).send('Webhook signature verification failed'); } switch (event.type) { case 'membership.created': // Handle new membership break; case 'membership.billing-failure': // Trigger dunning flow break; case 'membership.cancelled': // Revoke member access break; } res.status(200).send('OK'); }); ``` ```python Python theme={null} from svix.webhooks import Webhook, WebhookVerificationError secret = "whsec_your_signing_secret" @app.route('/webhooks/appstle-memberships', methods=['POST']) def webhook(): headers = { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), } try: wh = Webhook(secret) event = wh.verify(request.data, headers) except WebhookVerificationError: return "Verification failed", 400 if event["type"] == "membership.billing-failure": # trigger dunning logic pass return "OK", 200 ``` Pass the **raw request body** to the signature verification function — before JSON parsing. Parsing the body first will cause verification to fail because the byte representation changes. For Ruby, Go, PHP, Java, and C# examples, see the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how). ## Retry schedule If your endpoint returns a non-`2xx` status code or times out, Svix retries with exponential backoff across 5 attempts over 3 days. You can view delivery history and manually replay events from your Appstle dashboard under **Settings → Webhooks → Message Logs**. ## Idempotency Webhooks can be delivered more than once. Use the `svix-id` header value as an idempotency key to safely deduplicate events in your database before processing. ```javascript theme={null} const messageId = req.headers['svix-id']; const alreadyProcessed = await db.processedEvents.exists({ id: messageId }); if (alreadyProcessed) { return res.status(200).send('OK'); // already handled } await db.processedEvents.create({ id: messageId }); // ... process the event ``` ## Local development Use [ngrok](https://ngrok.com/) to expose your local server for webhook testing: ```bash theme={null} ngrok http 3000 # Then add https://your-id.ngrok.io/webhooks/appstle-memberships as your endpoint ``` ## Troubleshooting | Issue | Solution | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Signature verification fails | Use the raw request body before JSON parsing. Verify you are using the correct signing secret from the dashboard. | | Endpoint timing out | Return `200 OK` immediately, then process the event asynchronously in a background job or queue. | | Not receiving events | Confirm the webhook integration is enabled in Settings and that your endpoint is publicly accessible via HTTPS. | | Duplicate events | Use the `svix-id` header as a deduplication key before processing. | # Get past orders Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-past-orders /subscription/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/past-orders Retrieves paginated list of past (processed) billing attempts for a subscription contract or customer. Includes successful orders, failed attempts, and skipped orders. **Filter Options:** - By contract ID: Get order history for specific subscription - By customer ID: Get all orders across all customer subscriptions - Pagination: Control page size and page number **Order Information:** - Billing attempt status (SUCCESS, FAILED, SKIPPED) - Shopify order ID for successful attempts - Billing date and processing date - Order total and line items - Error messages for failed attempts **Authentication:** Requires valid X-API-Key header # Get past orders report with detailed filtering Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-past-orders-report-with-detailed-filtering /subscription/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/past-orders/report Retrieves a detailed report of past billing attempts with advanced filtering options. This endpoint provides comprehensive data for analytics, troubleshooting, and reporting purposes. **Filter Options:** - By status: SUCCESS, FAILED, SKIPPED - By date range: Filter by billing date range - By attempt count: Filter by number of retry attempts - By contract status: Filter by subscription contract status - By contract ID: Get report for specific subscription **Response Includes:** - Detailed billing attempt information - Shopify exception details for failed attempts (optional) - Retry attempt count - Processing timestamps - Order totals and line items **Authentication:** Requires valid X-API-Key header # Get upcoming orders (top orders) Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/get-upcoming-orders-top-orders /subscription/admin-api-swagger.json get /api/external/v2/subscription-billing-attempts/top-orders Retrieves upcoming (queued) billing attempts for a subscription contract or customer. Returns the next scheduled orders that have not yet been processed. **Query Options:** - Filter by contract ID to see upcoming orders for a specific subscription - Filter by customer ID to see all upcoming orders for a customer - Results are ordered by billing date (earliest first) **Use Cases:** - Display "Your Next Order" in customer portal - Show upcoming delivery schedule - Calculate upcoming charges - Preview next order contents **Authentication:** Requires valid X-API-Key header # Reschedule a billing attempt to a new date Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/reschedule-a-billing-attempt-to-a-new-date /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/reschedule-order/{id} Changes the scheduled billing date for a billing attempt. This allows customers to adjust when their next order will be processed. **Rescheduling Options:** - Move to earlier date (if allowed by shop settings) - Move to later date - Optionally reschedule all future orders by the same offset **Important Behaviors:** - Only QUEUED billing attempts can be rescheduled - New date must be in the future - Can affect future billing schedule if rescheduleFutureOrder is true - Activity logs are created for audit trail **Use Cases:** - Customer wants to delay next delivery - Customer wants to receive order earlier - Adjust delivery schedule to align with customer needs - Coordinate deliveries with customer vacation/travel **Authentication:** Requires valid X-API-Key header # Skip a specific order Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/skip-a-specific-order /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/skip-order/{id} Skips a specific billing attempt by ID. The order will not be processed on its scheduled date. This is useful when customers want to skip a particular delivery without canceling their subscription. **Important Behaviors:** - Only QUEUED billing attempts can be skipped - Skipped orders remain in the system with SKIPPED status - Future orders are not affected - Can be unskipped before the scheduled date if needed - Activity logs are created for audit trail **Use Cases:** - Customer is on vacation - Customer has excess inventory - Temporary delivery pause for one cycle **Authentication:** Requires valid X-API-Key header # Skip the next upcoming order for a subscription Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/skip-the-next-upcoming-order-for-a-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/skip-upcoming-order Skips the next scheduled billing attempt for a subscription contract without requiring the billing attempt ID. Automatically finds and skips the earliest QUEUED billing attempt. **Convenience Feature:** - No need to know the specific billing attempt ID - Automatically finds the next order - Ideal for "skip next order" functionality in customer portals **Use Cases:** - Simple "skip next delivery" button in customer portal - Quick skip without looking up billing attempt details - One-click skip functionality **Authentication:** Requires valid X-API-Key header # Trigger immediate billing for an order Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/trigger-immediate-billing-for-an-order /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/attempt-billing/{id} Immediately processes a billing attempt, creating an order in Shopify. This bypasses the scheduled billing date and processes the order right away. **Important Notes:** - Requires shop permission 'enableImmediatePlaceOrder' - Only QUEUED billing attempts can be processed - Creates an actual order in Shopify - Charges the customer's payment method immediately - Cannot be undone once processed **Use Cases:** - Customer requests early delivery - Process order immediately after resolving payment issue - Manual order processing for special cases **Authentication:** Requires valid X-API-Key header and shop permission # Unskip a previously skipped order Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/unskip-a-previously-skipped-order /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts/unskip-order/{id} Reverses a skip action on a billing attempt. The order will be restored to QUEUED status and will be processed on its scheduled date. **Important Notes:** - Only works on billing attempts with SKIPPED status - Must be done before the scheduled billing date - Cannot unskip after the billing date has passed - Activity logs are created for audit trail **Use Cases:** - Customer changes their mind about skipping - Correct accidental skip actions - Restore delivery after resolving temporary issue **Authentication:** Requires valid X-API-Key header # Update order note for a billing attempt Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/update-order-note-for-a-billing-attempt /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts-update-order-note/{id} Updates the order note (customer note) for a billing attempt. This note will be included when the order is created in Shopify. **Use Cases:** - Add delivery instructions - Include gift messages - Add special handling notes - Store customer preferences for this order **Important Notes:** - Only works on QUEUED billing attempts - Note is included in the Shopify order when created - Can be updated multiple times before order is processed **Authentication:** Requires valid X-API-Key header # Update subscription billing attempt Source: https://developers.appstle.com/subscription-admin-api/billing-&-payments/update-subscription-billing-attempt /subscription/admin-api-swagger.json put /api/external/v2/subscription-billing-attempts Updates an existing subscription billing attempt. This endpoint allows modification of billing attempt details such as billing date, order note, and other attributes. **Important Notes:** - Only QUEUED billing attempts can be updated - Cannot update attempts that are already processed or failed - Billing attempt must belong to the authenticated shop **Authentication:** Requires valid X-API-Key header # Create a new Build-A-Box subscription bundle Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/create-a-new-build-a-box-subscription-bundle /subscription/admin-api-swagger.json post /api/external/v2/build-a-box Creates a new subscription bundle (Build-A-Box) allowing customers to select and customize products for recurring deliveries. Build-A-Box enables a flexible subscription model where customers can create personalized product boxes. **What is Build-A-Box?** Build-A-Box is a subscription feature that allows customers to curate their own product bundles by selecting from a predefined set of products. This creates a highly personalized subscription experience where customers have full control over what they receive in each delivery cycle. **Key Features:** - **Product Selection**: Customers choose which products to include in their box - **Quantity Control**: Set minimum and maximum product quantities - **Flexible Configuration**: Define rules for product combinations - **Pricing Models**: Support for various pricing strategies (per-item, flat rate, tiered) - **Recurring Delivery**: Automatic fulfillment based on subscription frequency - **Customization Options**: Allow product swaps between delivery cycles **Configuration Options:** - **Bundle Settings**: - Bundle name and description - Unique handle for identification - Product pool (available products for selection) - Minimum/maximum number of products - Product quantity limits - **Pricing Configuration**: - Pricing type (per-product, flat rate, or tiered) - Discount rules - Promotional pricing - Currency settings - **Delivery Options**: - Subscription frequencies (weekly, bi-weekly, monthly, etc.) - Delivery intervals - Cut-off times for order modifications - Shipping methods - **Rules and Restrictions**: - Product combination rules - Category restrictions - Inventory requirements - Customer eligibility criteria **Build-A-Box Types:** 1. **Open Selection**: Customers can choose any products from the available pool 2. **Category-Based**: Products are organized into categories with selection rules 3. **Single Product**: Customers select variations of a single product type 4. **Tiered Boxes**: Different box sizes with varying product counts and pricing **Use Cases:** - **Coffee Subscription**: Customers select different coffee blends for monthly delivery - **Snack Boxes**: Build custom snack boxes from a variety of treats - **Beauty Boxes**: Choose skincare and makeup products based on preferences - **Meal Kits**: Select recipes and ingredients for weekly meal planning - **Pet Supply Boxes**: Customize toys, treats, and supplies for pets - **Supplement Subscriptions**: Create personalized vitamin and supplement regimens **Customer Workflow:** 1. Customer discovers Build-A-Box offering 2. Selects products from available options 3. Chooses delivery frequency 4. Reviews pricing and discounts 5. Completes subscription signup 6. Receives recurring deliveries 7. Can modify selections between delivery cycles **Important Notes:** - Each bundle must have a unique handle for identification - Product availability is validated at creation time - Pricing rules are applied based on bundle configuration - Bundles must be associated with at least one subscription frequency - Inventory levels should be checked for all included products - Bundle status (active/inactive) controls customer visibility **Best Practices:** - Set clear minimum and maximum product limits - Provide detailed product descriptions and images - Configure appropriate pricing that encourages subscriptions - Offer multiple delivery frequency options - Set reasonable inventory thresholds - Enable customer portal access for subscription management - Test bundle configurations before making them live **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Delete a Build-A-Box subscription bundle Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/delete-a-build-a-box-subscription-bundle /subscription/admin-api-swagger.json delete /api/external/v2/build-a-box/{id} Permanently removes a Build-A-Box subscription bundle from your shop. This operation is irreversible and will prevent new customers from subscribing to this bundle. However, existing subscriptions using this bundle will remain active. **Important Deletion Rules:** 1. **Active Subscriptions Check**: The system prevents deletion if there are active subscriptions using this bundle 2. **Alternative Approach**: If active subscriptions exist, you must first deactivate the bundle instead of deleting it 3. **Shop Ownership**: You can only delete bundles that belong to your authenticated shop 4. **Irreversible**: Once deleted, the bundle configuration cannot be recovered 5. **Clean Deletion**: Only bundles with zero associated subscriptions can be permanently removed **Deletion Workflow:** ``` 1. Request deletion of bundle with ID 2. System checks for shop ownership 3. System counts active subscriptions using this bundle 4. If subscriptions exist → Returns error with subscription count 5. If no subscriptions → Deletes bundle permanently 6. Returns success (204 No Content) ``` **When Deletion is Blocked:** The system will prevent deletion and return a 400 error if: - One or more active subscriptions are using this bundle - The error message will include the exact count of active subscriptions - Example: "Deleting Build-A-Box is not possible, 47 subscriptions found. You may deactivate the Build-A-Box." **Recommended Workflow for Active Bundles:** If you want to stop offering a bundle that has active subscribers: 1. **Update** the bundle to set `active: false` (this prevents new subscriptions) 2. **Monitor** existing subscriptions until they naturally expire or are cancelled 3. **Migrate** customers to a different bundle if needed 4. **Delete** the bundle once all subscriptions have been resolved **Use Cases:** - **Testing Cleanup**: Remove test bundles created during development - **Failed Configurations**: Delete bundles that were incorrectly set up - **Seasonal Offerings**: Remove time-limited bundles after the season ends (if no subscriptions) - **Product Discontinuation**: Clean up bundles for discontinued product lines - **Duplicate Bundles**: Remove accidentally created duplicate configurations **Best Practices:** - **Check Before Deleting**: Always verify no active subscriptions exist - **Use Deactivation First**: Set bundles to inactive before attempting deletion - **Document Changes**: Keep records of when and why bundles were deleted - **Backup Configuration**: Save bundle settings before deletion for potential recreation - **Communicate Changes**: Inform team members about bundle removals - **Consider Archiving**: For historical reference, deactivate rather than delete when possible **What Happens After Deletion:** - Bundle configuration is permanently removed from the database - Bundle ID becomes available for reuse (though not recommended) - Bundle handle becomes available for new bundles - External integrations referencing this bundle will receive 404 errors - Analytics and historical data may reference the deleted bundle ID **Error Prevention:** Before attempting deletion, you can: 1. Query active subscriptions to check for bundle usage 2. Set the bundle to inactive status first 3. Wait for all subscriptions to complete or migrate customers 4. Then proceed with deletion **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Generate discount code for bundle Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/generate-discount-code-for-bundle /subscription/admin-api-swagger.json put /api/external/v2/subscription-bundlings/discount/{token} Generates a discount code for a subscription bundle. This endpoint creates a Shopify discount code that can be applied to bundle subscriptions, typically used during the checkout process. **Discount Code Generation:** - Creates a unique discount code in Shopify - Associates the discount with the bundle - Configures discount rules and limitations - Sets expiration dates if specified - Applies usage limits if configured **Discount Configuration Options:** - Discount type (percentage, fixed amount, or free shipping) - Discount value/amount - Minimum purchase requirements - Maximum usage count - Expiration date - Customer eligibility rules **Use Cases:** - Promotional campaigns for bundles - Welcome discounts for new subscribers - Loyalty rewards for existing customers - Special offers and limited-time promotions - Partner/affiliate discount codes **Important Notes:** - Discount codes are created in Shopify and follow Shopify's discount rules - Codes can be single-use or multi-use depending on configuration - Expired or depleted codes cannot be reused **Authentication:** Requires valid X-API-Key header # Get bundle details by handle Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/get-bundle-details-by-handle /subscription/admin-api-swagger.json get /api/external/v2/subscription-bundlings/external/get-bundle/{handle} Retrieves complete details for a subscription bundle using its unique handle. This endpoint returns the bundle configuration, included products, pricing, and available subscription options. **Bundle Information Returned:** - Bundle name and description - Product selections with quantities - Pricing and discounts - Available subscription frequencies - Bundle status and availability - Product images and details **Use Cases:** - Display bundle details on product pages - Show bundle contents in customer portal - Fetch bundle configuration for cart/checkout - Integration with external systems - Build custom bundle selection interfaces **Bundle Handle:** The handle is a unique, URL-friendly identifier for the bundle. It's typically generated when the bundle is created and remains constant throughout the bundle's lifecycle. **Authentication:** Requires valid X-API-Key header # Get subscription bundle settings (Build-a-Box configuration) Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/get-subscription-bundle-settings-build-a-box-configuration /subscription/admin-api-swagger.json get /api/external/v2/subscription-bundle-settings/{id} Retrieves the configuration settings for subscription bundles (also known as Build-a-Box or customizable subscription boxes). This endpoint returns all settings that control how customers can create and manage their custom subscription bundles. **What are Subscription Bundles (Build-a-Box)?** Subscription bundles allow customers to create personalized subscription boxes by selecting multiple products from a curated collection. Instead of subscribing to a fixed product, customers build their own custom bundle that gets delivered on a recurring basis. This is perfect for coffee subscriptions, snack boxes, beauty boxes, supplement packs, and any product category where variety and personalization drive customer satisfaction. **Bundle Configuration Settings:** **1. Product Selection Rules:** - Minimum number of products required in bundle - Maximum number of products allowed in bundle - Product categories available for selection - Variant selection constraints - Quantity limits per product - Total bundle value constraints (min/max price) **2. Bundle Behavior:** - Allow customers to modify bundle between orders - Lock bundle after first order - Enable automatic product rotation - Allow quantity adjustments - Substitution rules when products unavailable **3. Pricing & Discounts:** - Bundle pricing model (fixed price vs. sum of products) - Volume discounts based on bundle size - Tiered pricing structures - Promotional pricing for bundles - Discount application rules **4. UI/UX Settings:** - Bundle builder interface layout - Product display format (grid/list) - Images and descriptions shown - Filter and search options - Preview and summary display - Mobile responsiveness settings **5. Fulfillment Options:** - Bundling method (ship together vs. separate) - Packaging preferences - Custom box selection - Gift message options - Delivery instructions **6. Customer Experience:** - Welcome flow for new bundle subscribers - Modification window before each order - Reminder notifications to update bundle - Recommendation engine settings - Save favorite bundle configurations **Common Bundle Types:** **Coffee Subscription Bundle:** - Select 3-5 coffee varieties per month - Choose roast levels, origins, or flavors - Option to rotate selections monthly - Fixed price regardless of selection **Snack Box Bundle:** - Choose 10 items from 50+ snacks - Category limits (e.g., max 3 sweet, 7 savory) - Dietary restriction filters (vegan, gluten-free) - Tiered pricing based on bundle size **Beauty Box Bundle:** - Select products from skincare, makeup, haircare categories - Minimum 5 products, maximum 8 products - Total value must be between $40-$100 - Can swap 2 products each month **Vitamin/Supplement Bundle:** - Build personalized supplement pack - Health goal-based recommendations - Compatibility checking (ingredient interactions) - Fixed monthly price for up to 5 supplements **Use Cases:** - Configure bundle builder in customer portal - Validate customer bundle selections against rules - Display bundle configuration options during signup - Integrate with checkout to show bundle pricing - Build custom bundle management interfaces - Sync bundle settings with mobile apps - Generate bundle recommendation algorithms **Important Notes:** - Settings apply to all subscription bundles in the shop - Changes affect new bundles immediately - Existing bundles maintain their original configuration unless migrated - Product availability is checked in real-time during bundle creation - Bundle modifications may have cutoff times before order processing **Best Practices:** - Set reasonable min/max limits to balance choice and complexity - Provide clear guidance on bundle building process - Use category limits to ensure variety - Enable modification windows to boost engagement - Offer curated bundle templates as starting points - Implement substitution logic for out-of-stock items - Test bundle builder UX across devices **Authentication:** Requires valid X-API-Key header # Retrieve a Build-A-Box bundle by ID Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/retrieve-a-build-a-box-bundle-by-id /subscription/admin-api-swagger.json get /api/external/v2/build-a-box/{id} Fetches the complete configuration and details of a specific Build-A-Box subscription bundle using its unique identifier. This endpoint returns all bundle settings including products, pricing, rules, and delivery options. **What You'll Receive:** - **Complete Bundle Configuration**: All settings and properties - **Product Information**: Full list of available products with details - **Pricing Structure**: Discount rules, pricing type, and promotional settings - **Delivery Options**: Subscription frequencies and delivery intervals - **Business Rules**: Product limits, combination rules, and restrictions - **Status Information**: Active/inactive state and timestamps - **Customization Settings**: Allow one-time purchases, product swaps, etc. **Use Cases:** 1. **Bundle Management**: Retrieve current bundle settings for review or editing 2. **Integration Sync**: Synchronize bundle data with external systems 3. **Customer Portal**: Display bundle options and configurations 4. **Audit & Reporting**: Track bundle configurations over time 5. **Validation**: Verify bundle setup before making changes 6. **Cloning**: Retrieve settings to duplicate a successful bundle 7. **Troubleshooting**: Debug subscription issues by reviewing bundle config **Response Details:** The response includes comprehensive information about the bundle: - **Bundle Metadata**: ID, shop, name, handle, unique reference - **Product Pool**: All products available for customer selection - **Selection Rules**: Min/max product counts, quantity limits - **Pricing Configuration**: Discount percentages, pricing models - **Subscription Options**: Available delivery frequencies - **Display Settings**: Product view styles, custom HTML, button text - **Advanced Features**: Third-party rules, inventory tracking, selection types - **Timestamps**: Creation and last update dates **Authorization:** - You can only retrieve bundles that belong to your authenticated shop - The system verifies shop ownership before returning bundle data - Returns 404 if bundle doesn't exist or doesn't belong to your shop **Integration Tips:** - Cache bundle data to reduce API calls for frequently accessed bundles - Use this endpoint to verify bundle existence before creating subscriptions - Combine with update endpoint for edit workflows (get → modify → update) - Check the `active` field to determine if the bundle is available to customers - Review `availableProducts` array to ensure product inventory availability **Performance Considerations:** - Response size varies based on number of products in the bundle - Bundles with extensive custom HTML or large product pools return more data - Consider requesting only necessary fields if partial data is sufficient - Use batch operations if retrieving multiple bundles **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Retrieve Build-A-Box bundle details by handle for customer storefront Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/retrieve-build-a-box-bundle-details-by-handle-for-customer-storefront /subscription/admin-api-swagger.json get /api/external/v2/build-a-box/{handle} Fetches comprehensive Build-A-Box bundle configuration using a user-friendly handle (URL slug) instead of numeric ID. This endpoint is specifically designed for customer-facing storefronts and integration scenarios where you want to reference bundles by their human-readable handles rather than database IDs. **Key Differences from GET /{id}:** - **Lookup Method**: Uses bundle handle (e.g., 'premium-coffee-box') instead of numeric ID (e.g., 12345) - **Response Format**: Returns SubscriptionBundlingResponseV3 with enhanced structure - **Customer Focus**: Optimized for storefront display and customer selection workflows - **Bundle + Subscription**: Returns both bundle details AND associated subscription plan information - **Public Access**: Designed for integration in customer-facing applications **What is a Bundle Handle?** A handle is a URL-friendly identifier for a bundle: - Format: lowercase letters, numbers, and hyphens only - Example: `premium-coffee-selection`, `monthly-snack-box`, `beauty-essentials` - Stable: Unlike IDs, handles remain consistent across environments - SEO-Friendly: Can be used in customer-facing URLs - Human-Readable: Easy to remember and communicate **Response Structure (SubscriptionBundlingResponseV3):** ```json { "bundle": { // Complete bundle configuration "id": 45678, "bundleName": "Premium Coffee Selection", "bundleHandle": "premium-coffee-selection", "description": "...", "products": [...], "pricing": {...}, "active": true }, "subscription": { // Associated subscription plan details "planId": 98765, "frequencies": [...], "deliveryOptions": {...}, "sellingPlan": {...} } } ``` **Primary Use Cases:** 1. **Storefront Integration**: Display bundle details on product pages 2. **Customer Portal**: Allow customers to browse available bundles 3. **Landing Pages**: Create dedicated pages for specific bundles using handles 4. **Marketing Campaigns**: Link directly to bundles with memorable URLs 5. **Cross-Platform Sync**: Use stable handles for multi-platform integrations 6. **Mobile Apps**: Retrieve bundle data using consistent identifiers 7. **External Integrations**: Third-party systems can reference bundles by handle **When to Use Handle vs ID:** - **Use Handle** when: - Building customer-facing features - Creating shareable URLs - Syncing across environments (dev → staging → prod) - External systems need stable references - SEO and user experience are priorities - **Use ID** when: - You already have the numeric ID from another API response - Performing administrative operations - Internal system integration - Optimizing for query performance **Availability Validation:** The endpoint validates that: - Bundle exists and belongs to the specified shop - Bundle has an associated subscription configuration - If validation fails, returns 404 Not Found - Both bundle AND subscription must be present for success response **Integration Examples:** **Storefront Display:** ```javascript // Fetch bundle for display on product page const handle = 'premium-coffee-selection'; const response = await fetch( `/api/external/v2/build-a-box/${handle}`, { headers: { 'X-API-Key': 'your-api-key' } } ); const { bundle, subscription } = await response.json(); displayBundleOptions(bundle, subscription); ``` **Best Practices:** - **Handle Naming**: Use descriptive, SEO-friendly handles - **Caching**: Cache bundle data with handle as key for better performance - **Error Handling**: Always check for 404 - bundle may be deleted or deactivated - **Environment Sync**: Use handles to maintain consistency across environments - **URL Structure**: Incorporate handles into clean, readable URLs - **Documentation**: Document handle conventions for your team **Performance Considerations:** - Handle lookups may be slightly slower than ID lookups - Response includes full bundle + subscription data (larger payload) - Consider caching for frequently accessed bundles - Returns complete product catalog for the bundle **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Update an existing Build-A-Box subscription bundle Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/update-an-existing-build-a-box-subscription-bundle /subscription/admin-api-swagger.json put /api/external/v2/build-a-box Updates an existing Build-A-Box subscription bundle with new configuration settings, product selections, pricing rules, or other bundle attributes. This endpoint allows you to modify any aspect of a previously created bundle while maintaining its unique identifier and shop association. **What Can Be Updated:** - **Bundle Information**: Name, description, handle, and display settings - **Product Configuration**: Available products, product pool, and selection rules - **Quantity Limits**: Minimum and maximum product counts per box - **Pricing Settings**: Pricing type, discount rules, and promotional offers - **Delivery Options**: Subscription frequencies and delivery intervals - **Business Rules**: Product combination rules, category restrictions - **Status**: Active/inactive state for customer visibility - **Customization Options**: Allow product swaps, one-time purchases **Update Behavior:** - The bundle ID must be provided in the request body - Shop ownership is verified - you can only update bundles belonging to your shop - All fields in the request body will update the corresponding bundle properties - Partial updates are supported - only include fields you want to change - The update is atomic - either all changes succeed or none are applied - Existing subscriptions using this bundle are not automatically affected **Impact on Active Subscriptions:** - Changes to product availability affect future customer selections - Pricing updates apply to new subscriptions but not existing ones by default - Quantity limit changes are enforced on next customer modification - Frequency changes only affect new subscriptions - Deactivating a bundle prevents new subscriptions but maintains existing ones **Common Update Scenarios:** 1. **Add/Remove Products**: Update the available product pool 2. **Adjust Pricing**: Change discount percentages or pricing models 3. **Modify Limits**: Update minimum/maximum product selection rules 4. **Change Frequencies**: Add or remove subscription interval options 5. **Update Content**: Modify bundle name, description, or images 6. **Toggle Status**: Activate or deactivate bundle availability 7. **Refine Rules**: Adjust product combination or category restrictions **Best Practices:** - Verify product availability before adding to the bundle - Test pricing changes with sample calculations - Communicate bundle changes to existing subscribers - Use inactive status for testing changes before making them live - Keep bundle handles consistent for external integrations - Document major changes for customer support reference - Consider seasonal or promotional updates to keep offerings fresh **Validation Rules:** - Bundle ID is required and must exist - Shop must match the authenticated shop (cannot transfer bundles) - All referenced products must be valid and available - Minimum product count cannot exceed maximum product count - Pricing values must be non-negative - Bundle handle should remain unique across all bundles **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Update subscription bundling configuration Source: https://developers.appstle.com/subscription-admin-api/build-a-box-&-bundles/update-subscription-bundling-configuration /subscription/admin-api-swagger.json put /api/external/v2/subscription-bundling/update Updates the configuration for one or more subscription bundles (Build-a-Box). This endpoint allows bulk updates to bundle product selections, quantities, and configurations. **Subscription Bundling/Build-a-Box:** Subscription bundling allows customers to create custom product bundles that are delivered on a recurring basis. Customers can select multiple products to be included in each delivery, creating a personalized subscription box. **Update Capabilities:** - Modify product selections within a bundle - Update product quantities - Change bundle configuration settings - Bulk update multiple bundles at once **Use Cases:** - Customer updates their bundle product selections - Swap products in an existing bundle - Adjust quantities for products in the bundle - Programmatically manage bundle configurations **Important Notes:** - Updates are identified by bundle handle (unique identifier) - Only active bundles can be updated - Product availability is validated before update **Authentication:** Requires valid X-API-Key header # Get customer retention activities Source: https://developers.appstle.com/subscription-admin-api/customer-retention/get-customer-retention-activities /subscription/admin-api-swagger.json get /api/external/v2/customer-retention-activities Retrieves a paginated, filterable list of customer retention activities for the authenticated shop. A retention activity is recorded whenever a customer goes through the cancellation or pause flow and either accepts a retention offer (discount, pause, frequency change, product swap, etc.) or proceeds to cancel/pause, along with the reason they selected. **What is a Retention Activity?** Every time a subscriber interacts with the cancellation/pause save-flow, an activity record is created capturing the contract, the action taken (e.g. offer shown, offer accepted, cancelled, paused), the event source (CANCELLATION or PAUSE), the selected reason, any discount applied, and the resulting status. **Filtering:** - `contractId` - restrict results to a single subscription contract - `eventSource` - `CANCELLATION` or `PAUSE` - `status` - status of the retention activity (e.g. PENDING, COMPLETED) - `retentionAction` - the action recorded (e.g. offer type accepted or DECLINED) - `retentionReason` - the cancellation/pause reason the customer selected - `fromDay` / `toDay` - ISO-8601 date-time bounds on `activityOn` **Pagination:** Standard Spring pagination parameters are supported: `page` (0-indexed), `size`, and `sort` (e.g. `sort=id,desc`). Pagination metadata (total count, total pages, link headers) is returned in the response headers. **Use Cases:** - Build custom churn/retention dashboards and reports - Sync retention activity into an external CRM or data warehouse - Audit which retention offers are converting for a given contract or reason - Feed win-back campaign automation with recent cancellation reasons **Authentication:** Requires a valid API key, passed via the `X-API-Key` header (preferred) or the `api_key` query parameter (deprecated). # Generate customer portal access link by customer ID or email Source: https://developers.appstle.com/subscription-admin-api/customers/generate-customer-portal-access-link-by-customer-id-or-email /subscription/admin-api-swagger.json get /api/external/v2/manage-subscription-link Generates a secure, time-limited magic link that allows customers to access their subscription management portal. This endpoint supports lookup by either customer ID or email address, making it flexible for different integration scenarios. **Key Features:** - **Dual Lookup**: Find customer by ID or email - **Auto Customer Discovery**: Automatically finds customer from email - **Secure Tokens**: Encrypted tokens with 2-hour expiration - **Custom Domains**: Supports shop's public domain - **Zero-Auth Access**: Customers don't need passwords **Customer Lookup Logic:** **Option 1: By Customer ID (Preferred)** ``` GET /api/external/v2/manage-subscription-link?customerId=12345 ``` - Direct lookup by Shopify customer ID - Fastest and most reliable method - No ambiguity **Option 2: By Email** ``` GET /api/external/v2/manage-subscription-link?emailId=customer@example.com ``` - Searches for customer by email in subscription database - Finds customer ID automatically - If not found: Returns error **Validation Rules:** - Either `customerId` OR `emailId` must be provided - Cannot provide both (customerId takes precedence) - Email must match a customer with subscriptions - Customer must belong to authenticated shop **Token Generation:** **Token Contents:** - Encrypted customer ID - Shop domain - Generation timestamp - Expiration time (2 hours) **Security Features:** - Cryptographically secure encryption - Cannot be forged or modified - Automatic expiration after 2 hours - Single-use recommended (though not enforced) - Tied to specific shop and customer **Generated URL Structure:** ``` https://[shop-domain]/[manage-subscriptions-path]?token=[encrypted-token] ``` **Example URLs:** ``` https://mystore.com/tools/recurring/customer_portal?token=eyJhbGc... https://shop.myshopify.com/tools/recurring/customer_portal?token=eyJhbGc... ``` **Use Cases:** **1. Email Campaigns:** - Add "Manage Subscription" button to transactional emails - Include in billing reminder emails - Send in order confirmation emails - Add to marketing campaigns **2. Customer Support:** - Provide customers quick portal access - Avoid "forgot password" issues - Enable instant self-service - Reduce support ticket volume **3. Post-Purchase Flows:** - Thank you page portal links - First order welcome emails - Onboarding email sequences - Re-engagement campaigns **4. Account Management:** - SMS notifications with portal links - Push notification deep links - Customer dashboard integrations - Third-party app integrations **Response Format:** ```json { "manageSubscriptionLink": "https://mystore.com/tools/recurring/customer_portal?token=eyJhbGciOiJIUzI1NiJ9...", "tokenExpirationTime": "2024-03-15T14:30:00Z" } ``` **Response Fields:** - `manageSubscriptionLink`: Complete URL ready to use - `tokenExpirationTime`: ISO 8601 timestamp when token expires **Integration Examples:** **Email Template:** ```javascript const response = await fetch( `/api/external/v2/manage-subscription-link?emailId=${customerEmail}`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); const emailHtml = `

Hi ${customerName},

Manage your subscription:

Manage Subscription

Link expires ${formatDate(response.tokenExpirationTime)}

`; ``` **SMS Notification:** ```javascript const { manageSubscriptionLink } = await getPortalLink(customerId); const shortUrl = await shortenUrl(manageSubscriptionLink); await sendSMS(customerPhone, `Your subscription ships tomorrow! Manage it here: ${shortUrl}` ); ``` **Important Considerations:** **Token Expiration:** - Tokens expire after exactly 2 hours - Generate new token if expired - Don't store tokens long-term - Best practice: Generate on-demand **Domain Selection:** - Uses shop's `publicDomain` if configured - Falls back to Shopify domain (.myshopify.com) - Respects custom domain settings - Maintains brand consistency **Customer Lookup Errors:** - Email not found: Returns 400 error - Invalid customer ID: Returns error - No parameters provided: Returns 400 - Both parameters provided: Uses customerId **Security Notes:** - Tokens cannot be used across different shops - Cannot be used for different customers - Tampering invalidates token - Consider rate limiting token generation **Best Practices:** 1. **Generate On-Demand**: Create tokens when needed, not in advance 2. **Use HTTPS**: Always serve links over HTTPS 3. **Show Expiry**: Inform customers when link expires 4. **URL Shortening**: Use URL shorteners for SMS/print materials 5. **Track Usage**: Monitor which emails drive portal visits 6. **Prefer Customer ID**: Use customerId when available for faster lookup **Comparison with /manage-subscription-link/{customerId}:** - This endpoint: Flexible lookup (ID or email) - Path parameter version: Customer ID only - Both generate identical tokens - Use this for email-based flows **Authentication:** Requires valid X-API-Key header # Generate customer portal link Source: https://developers.appstle.com/subscription-admin-api/customers/generate-customer-portal-link /subscription/admin-api-swagger.json get /api/external/v2/manage-subscription-link/{customerId} Generates a secure, time-limited link that allows customers to access their subscription management portal. This link provides customers with self-service capabilities to manage their subscriptions without requiring login credentials. **Key Features:** - Generates unique encrypted token for customer authentication - Token expires after 2 hours for security - Direct access to subscription management without password - Uses store's custom domain when available - Supports white-label customer portals **Token Security:** - Token contains encrypted customer ID, shop, and timestamp - Cannot be reused after expiration - Unique token generated for each request - Cryptographically secure encryption **Customer Portal Access:** Once customers click the link, they can: - View all active subscriptions - Update payment methods - Change delivery addresses - Modify product quantities - Pause or cancel subscriptions - Update delivery schedules - Apply discount codes - View order history **Use Cases:** - Email campaigns with 'Manage Subscription' CTAs - Customer service providing quick access - Post-purchase email flows - Account management integrations - Reducing support ticket volume **URL Structure:** The generated URL follows this pattern: `https://[store-domain]/[manage-subscription-path]?token=[encrypted-token]` **Important Notes:** - Links are single-use and expire after 2 hours - New link must be generated after expiration - Customer ID must be valid and active - Store must have customer portal configured **Authentication:** Requires valid X-API-Key header # Get customer details with subscriptions Source: https://developers.appstle.com/subscription-admin-api/customers/get-customer-details-with-subscriptions /subscription/admin-api-swagger.json get /api/external/v2/subscription-customers/{id} Retrieves comprehensive customer information including their subscription contracts. This endpoint provides customer profile data along with a paginated list of their active and historical subscriptions. **Key Features:** - Customer profile information (name, email, phone, addresses) - Complete subscription history with pagination - Payment method details - Customer account state and metadata - Order history related to subscriptions - Customer tags and notes **Subscription Data Included:** - All subscription contracts (active, paused, cancelled) - Contract details with line items - Billing and delivery policies - Next billing dates and amounts - Applied discounts **Pagination:** - Subscription contracts are paginated - Default page size varies by Shopify plan - Use cursor for subsequent pages - Cursor-based pagination ensures consistency **Use Cases:** - Customer service representatives viewing customer details - Integration with CRM systems - Customer analytics and reporting - Subscription management interfaces - Billing reconciliation **Important Notes:** - Customer ID is numeric (without gid:// prefix) - Returns null if customer not found - Includes both active and inactive subscriptions - Payment methods may be masked for security **Authentication:** Requires valid X-API-Key header # Get customer payment methods from Shopify Source: https://developers.appstle.com/subscription-admin-api/customers/get-customer-payment-methods-from-shopify /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/shopify/customer/{customerId}/payment-methods Retrieves all payment methods associated with a customer directly from Shopify's payment API. This endpoint returns detailed information about stored payment instruments including credit/debit cards, digital wallets, and other payment methods that the customer can use for subscription billing. **What This Endpoint Does:** Queries Shopify's GraphQL API to fetch the customer's payment methods, including active instruments and optionally revoked (expired, removed, or failed) payment methods. This provides real-time payment method data directly from Shopify's payment vault. **Payment Method Types Supported:** **Credit/Debit Cards:** - Visa, Mastercard, American Express, Discover - Card last 4 digits - Expiry month and year - Card brand and type - Billing address associated with card **Digital Wallets:** - Shop Pay - Apple Pay - Google Pay - PayPal (when stored) **Alternative Payment Methods:** - Bank accounts (ACH, SEPA) - Buy Now Pay Later instruments - Store credit **Payment Method Information Returned:** **For Each Payment Method:** - **Payment Instrument ID**: Unique identifier (used for updates) - **Display Name**: Human-readable name (e.g., "Visa ending in 4242") - **Payment Type**: Card, wallet, bank account, etc. - **Status**: ACTIVE, REVOKED, EXPIRED, FAILED - **Is Default**: Whether this is the customer's default payment method - **Last 4 Digits**: For cards and bank accounts - **Expiry Date**: For cards (month/year) - **Brand**: Visa, Mastercard, Amex, etc. - **Billing Address**: Address associated with payment method - **Created Date**: When payment method was added **Query Parameters:** **allowRevokedMethod (optional, default: false):** - `false`: Returns only active, usable payment methods - `true`: Returns both active AND revoked payment methods **Revoked Payment Methods:** Include expired cards, deleted payment methods, failed instruments, and customer-removed methods. Useful for historical records and troubleshooting, but cannot be used for new billing attempts. **Use Cases:** **1. Customer Portal:** - Display saved payment methods - Allow customer to select default payment method - Show payment method update/delete options - Validate payment methods before subscription modification **2. Subscription Management:** - Verify customer has valid payment method before creating subscription - Check payment method expiry before next billing - Identify subscriptions at risk due to expiring cards - Prompt customer to update payment if needed **3. Payment Method Updates:** - List available payment methods for customer selection - Identify payment instrument ID for update operations - Validate payment method before switching subscription **4. Troubleshooting & Support:** - Debug payment failures - Verify which payment method is being used - Check if payment method is expired or revoked - Assist customer with payment issues **5. Analytics & Alerts:** - Track payment methods approaching expiry - Send proactive notifications to update cards - Analyze payment method distribution - Identify customers with no valid payment methods **Response Structure:** Returns CustomerPaymentMethodsQuery.PaymentMethods object from Shopify GraphQL: ```json { "nodes": [ { "id": "gid://shopify/CustomerPaymentMethod/abc123", "instrument": { "__typename": "CustomerCreditCard", "brand": "VISA", "lastDigits": "4242", "expiryMonth": 12, "expiryYear": 2025, "name": "John Doe" }, "revokedAt": null, "revokedReason": null, "subscriptionContracts": [...] } ] } ``` **Common Scenarios:** **Scenario 1: Customer with multiple cards** Returns array with multiple payment method objects, each representing a stored card. **Scenario 2: Customer with no payment methods** Returns empty nodes array - customer needs to add payment method. **Scenario 3: Expired card included (allowRevokedMethod=true)** Returns both active cards and expired/revoked cards with revokedAt timestamp. **Important Considerations:** **Data Source:** - Queries Shopify API in real-time (not Appstle database) - Always returns current Shopify payment method state - Subject to Shopify API rate limits **Performance:** - Response time: 300-800ms (depends on Shopify API) - Slower than database queries - Consider caching for non-critical displays **Security:** - Never returns full card numbers (PCI compliance) - Returns only last 4 digits - CVV is never stored or returned - Payment instrument IDs are tokenized references **Privacy:** - Customer ID validated against shop - Cannot query payment methods from other shops - Requires appropriate API permissions **Best Practices:** 1. **Default to Active Only**: Use `allowRevokedMethod=false` for payment selection UIs 2. **Check Expiry Dates**: Validate card expiry before using for subscriptions 3. **Cache Responsibly**: Cache for short periods (5-10 min) to reduce API calls 4. **Handle Empty Response**: Always handle case where customer has no payment methods 5. **Show User-Friendly Names**: Display brand and last 4 (e.g., "Visa •••• 4242") 6. **Indicate Default**: Highlight the default payment method clearly **Integration Examples:** **Example 1: Display payment methods in customer portal** ```javascript const paymentMethods = await fetch( `/api/external/v2/subscription-contract-details/shopify/customer/${customerId}/payment-methods`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); paymentMethods.nodes.forEach(pm => { if (pm.instrument.__typename === 'CustomerCreditCard') { console.log(`${pm.instrument.brand} ending in ${pm.instrument.lastDigits}`); if (pm.instrument.expiryYear < currentYear) { console.warn('Card expired!'); } } }); ``` **Example 2: Check for valid payment before creating subscription** ```javascript const paymentMethods = await fetch(...); const hasValidPayment = paymentMethods.nodes.some(pm => !pm.revokedAt && pm.instrument.expiryYear >= currentYear ); if (!hasValidPayment) { alert('Please add a valid payment method before subscribing'); } ``` **Related Endpoints:** - `PUT /api/external/v2/subscription-contracts-update-payment-method` - Update subscription payment method - `POST /api/external/v2/associate-shopify-customer-to-external-payment-gateways` - Add external payment method **Authentication:** Requires valid X-API-Key header # Get detailed subscription information for a customer Source: https://developers.appstle.com/subscription-admin-api/customers/get-detailed-subscription-information-for-a-customer /subscription/admin-api-swagger.json get /api/external/v2/subscription-customers-detail/valid/{customerId} Retrieves comprehensive subscription contract details for a specific customer, including subscription status, products, billing information, delivery schedules, and more. This endpoint returns full subscription objects with all associated data, making it ideal for displaying subscription management interfaces and detailed analytics. **What This Endpoint Returns:** Unlike the contract IDs endpoint which returns only numeric IDs, this endpoint provides complete SubscriptionContractDetailsDTO objects for each of the customer's subscriptions. Each object contains all information needed to display and manage a subscription. **Data Included in Response:** **Subscription Identity:** - Subscription contract ID (Shopify numeric ID) - Subscription GraphQL ID (Shopify GID format) - Internal Appstle database ID - Contract creation date **Subscription Status & Lifecycle:** - Current status (ACTIVE, PAUSED, CANCELLED, EXPIRED, FAILED) - Status reason (why paused/cancelled) - Next billing date and time - Contract anchor date - Cancellation date (if applicable) - Current billing cycle number - Min/max cycle limits **Billing Configuration:** - Billing interval (WEEK, MONTH, YEAR) - Billing interval count (e.g., every 2 weeks) - Billing policy (pricing details) - Currency code - Recurring total price - Applied discounts and pricing policies **Delivery Configuration:** - Delivery interval (WEEK, MONTH, YEAR) - Delivery interval count - Delivery method (SHIPPING, PICK_UP, LOCAL_DELIVERY) - Delivery policy details - Delivery price **Products & Line Items:** - All subscribed products and variants - Product titles, SKUs, and images - Quantities per product - Individual line item prices - Product-specific discounts - Line item attributes and custom fields **Customer Information:** - Customer ID and GraphQL ID - Customer name and email - Customer acceptance status **Address Details:** - Billing address (full address object) - Shipping address (full address object) - Address validation status **Payment Information:** - Payment instrument type - Payment method details (card last 4, etc.) - Payment gateway used **Order & Fulfillment:** - Last order ID and details - Last order date - Order note - Fulfillment status **Additional Metadata:** - Custom note attributes - Tags and labels - Internal flags and settings - Selling plan ID and group ID - Shop domain **Use Cases:** **1. Customer Portal:** - Display all subscriptions on customer dashboard - Show subscription details page - Enable subscription management actions - Display upcoming order information **2. Admin Dashboard:** - View customer's complete subscription portfolio - Analyze subscription health and status - Identify at-risk or high-value subscriptions - Generate customer subscription reports **3. Customer Support:** - Quick access to all customer subscription details - Troubleshoot billing or delivery issues - Verify subscription configurations - Assist with subscription modifications **4. Analytics & Reporting:** - Calculate customer lifetime value - Analyze subscription mix per customer - Track subscription frequency distribution - Identify cross-sell/upsell opportunities **5. Integration & Automation:** - Sync subscription data to CRM/analytics platforms - Trigger workflows based on subscription details - Build custom reporting dashboards - Export subscription data for analysis **Response Format:** Returns an array of SubscriptionContractDetailsDTO objects: ```json [ { "id": 789, "subscriptionContractId": 5234567890, "status": "ACTIVE", "nextBillingDate": "2024-03-15T00:00:00Z", "billingInterval": "MONTH", "billingIntervalCount": 1, "deliveryInterval": "MONTH", "deliveryIntervalCount": 1, "currencyCode": "USD", "currentTotalPrice": "49.99", "customerId": 12345, "customerEmail": "customer@example.com", "lineItems": [...], "billingAddress": {...}, "shippingAddress": {...} }, {...} ] ``` **Response Scenarios:** **Customer with no subscriptions:** ```json [] ``` Returns empty array with 200 OK status (not an error). **Customer with multiple subscriptions:** Array contains multiple subscription objects, each representing a separate subscription contract. **Performance Considerations:** **Response Size:** - Each subscription object can be 5-50 KB depending on line items - Customer with 10 subscriptions: ~100-500 KB response - Consider pagination or filtering for customers with 20+ subscriptions **Query Performance:** - Typical response time: 200-500ms - Slower for customers with many subscriptions or complex products - Database query is optimized with indexed lookups **Best Practices:** 1. **Cache Results**: Cache response data to minimize API calls 2. **Filter Client-Side**: Filter/sort subscriptions on client after retrieval 3. **Selective Display**: Don't display all fields if not needed 4. **Handle Empty Array**: Always gracefully handle case with no subscriptions 5. **Optimize Images**: Product images can be large - lazy load if displaying **Data Freshness:** - Data is retrieved from Appstle database (not real-time Shopify query) - Updated via webhooks with < 1 second lag typically - Use sync endpoint if data appears stale **Security Notes:** - Customer ID is validated against authenticated shop - Returns only subscriptions belonging to specified customer - Cannot access customers from other shops - Sensitive payment details (full card numbers) are never returned **Comparison with Other Endpoints:** **vs. GET /subscription-customers/valid/{customerId}:** - This endpoint: Returns complete subscription details - Valid contracts endpoint: Returns only contract IDs - Use this when you need full subscription information **vs. GET /subscription-contract-details:** - This endpoint: Filtered by single customer - Contract details endpoint: Query across all customers with filters - Use this for customer-specific views **vs. GET /subscription-contracts/contract-external/{contractId}:** - This endpoint: All contracts for a customer - Contract external endpoint: Single contract with Shopify raw data - Use this for customer overview, other for detailed single contract **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Get valid subscription contract IDs for a customer Source: https://developers.appstle.com/subscription-admin-api/customers/get-valid-subscription-contract-ids-for-a-customer /subscription/admin-api-swagger.json get /api/external/v2/subscription-customers/valid/{customerId} Retrieves a set of all valid (active, paused, or otherwise non-deleted) subscription contract IDs for a specific customer. This endpoint returns only the contract IDs without detailed subscription information, making it ideal for quick lookups and validation checks. **What Are Valid Subscription Contracts?** Valid contracts are subscriptions that exist in the system and haven't been permanently deleted. This includes subscriptions in all states except those that have been hard-deleted or hidden from the system. The contract IDs returned represent subscriptions that can be queried, modified, or managed through other API endpoints. **Contract States Included:** **Active States:** - `ACTIVE` - Currently active and billing recurring subscriptions - `PAUSED` - Temporarily paused subscriptions (customer can resume) **Inactive States:** - `CANCELLED` - Cancelled subscriptions (retained for historical data) - `EXPIRED` - Subscriptions that reached their max cycle limit - `FAILED` - Subscriptions with payment failures (may be in dunning) **Excluded:** - Hard-deleted subscription records - Hidden subscriptions (marked for cleanup) - Subscriptions from test/development that were purged **Use Cases:** **1. Customer Portal:** - Check if customer has any subscriptions before showing portal - Display subscription count in dashboard - Validate customer access to subscription management **2. Integration & Automation:** - Pre-flight check before bulk operations - Verify customer has subscriptions before sending emails - Filter customers for targeted campaigns (has active subscriptions) **3. Validation & Security:** - Verify a specific contract ID belongs to a customer - Validate user permissions before showing subscription details - Check authorization for subscription modification requests **4. Analytics & Reporting:** - Count total subscriptions per customer - Identify customers with multiple subscriptions - Build customer segmentation based on subscription count **Response Format:** Returns a `Set` containing Shopify subscription contract IDs. ```json [123456789, 123456790, 123456791] ``` **Set Properties:** - **Unique values**: No duplicate contract IDs - **Unordered**: IDs are not in any specific order - **Numeric**: IDs are Long integers (not GraphQL GIDs) **Common Response Scenarios:** **Customer with multiple subscriptions:** ```json [5234567890, 5234567891, 5234567892, 5234567893] ``` **Customer with single subscription:** ```json [5234567890] ``` **Customer with no subscriptions:** ```json [] ``` **Integration Examples:** **Example 1: Check if customer has subscriptions** ```javascript const contractIds = await fetch('/api/external/v2/subscription-customers/valid/12345', { headers: { 'X-API-Key': 'your-api-key' } }).then(r => r.json()); if (contractIds.length > 0) { console.log(`Customer has ${contractIds.length} subscriptions`); // Show customer portal } else { console.log('Customer has no subscriptions'); // Redirect to create subscription page } ``` **Example 2: Validate contract ownership** ```javascript const customerId = 12345; const contractIdToVerify = 5234567890; const validContracts = await fetch(`/api/external/v2/subscription-customers/valid/${customerId}`, { headers: { 'X-API-Key': 'your-api-key' } }).then(r => r.json()); if (validContracts.includes(contractIdToVerify)) { console.log('Customer owns this contract - allowing access'); // Proceed with subscription modification } else { console.log('Unauthorized - contract does not belong to customer'); // Return 403 Forbidden } ``` **Performance Characteristics:** **Fast Response:** - Lightweight query (returns only IDs, not full subscription data) - Typical response time: 50-200ms - Suitable for real-time validation checks **Scalability:** - Efficient even for customers with 100+ subscriptions - Database query uses indexed customer ID field - Minimal network payload (just array of numbers) **Important Notes:** **Contract ID Format:** - Returns numeric Shopify contract IDs (e.g., `5234567890`) - NOT Shopify GraphQL GIDs (e.g., `gid://shopify/SubscriptionContract/...`) - Use these IDs with other Appstle API endpoints **Data Freshness:** - Returns data from Appstle database (not real-time Shopify query) - Data is updated via webhooks (typically < 1 second lag) - If data seems stale, use sync endpoint to refresh **Empty Response:** - Empty array `[]` means customer has no valid subscriptions - This is NOT an error - it's a valid response - Returns 200 OK with empty array (not 404) **Security & Authorization:** - Customer ID is validated against authenticated shop - Cannot query customers from other shops - API key must have customer read permissions **Best Practices:** 1. **Use for Validation**: Perfect for quick ownership/existence checks 2. **Cache Locally**: Cache results briefly to reduce API calls 3. **Check Empty Array**: Always handle empty array case gracefully 4. **Combine with Details**: Use this for initial check, then fetch full details if needed 5. **Avoid Polling**: Don't poll this endpoint repeatedly - use webhooks for updates **When to Use vs. Other Endpoints:** **Use this endpoint when you:** - Need just the contract IDs (not full subscription details) - Want to validate a customer has subscriptions - Need to verify contract ownership for authorization - Want fast, lightweight responses **Use subscription-customers-detail endpoint when you:** - Need full subscription details (status, products, billing dates, etc.) - Want to display subscription information to users - Need to make decisions based on subscription state **Related Endpoints:** - `GET /api/external/v2/subscription-customers-detail/valid/{customerId}` - Get full subscription details - `GET /api/external/v2/subscription-contract-details` - Query subscriptions with filters - `GET /api/external/v2/subscription-contracts/contract-external/{contractId}` - Get single contract details **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Retrieve customers with subscriptions Source: https://developers.appstle.com/subscription-admin-api/customers/retrieve-customers-with-subscriptions /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/customers Returns a paginated list of customers who have subscription contracts. Supports filtering by customer name, email, and subscription count. # Sync customer subscription details from Shopify Source: https://developers.appstle.com/subscription-admin-api/customers/sync-customer-subscription-details-from-shopify /subscription/admin-api-swagger.json get /api/external/v2/subscription-customers/sync-info/{customerId} Synchronizes customer subscription information from Shopify to the Appstle subscription system. This endpoint fetches the latest customer data from Shopify and updates the local database to ensure data consistency across platforms. **What Does This Endpoint Do?** This endpoint triggers a synchronization process that pulls customer subscription data from Shopify's GraphQL API and updates the Appstle subscription database with the latest information. It ensures that customer details, subscription statuses, and associated metadata are current and accurate. **Sync Process:** 1. **Data Retrieval**: Fetches customer subscription data from Shopify using their GraphQL API 2. **Validation**: Validates the retrieved data against existing records 3. **Update**: Updates customer subscription details in Appstle database 4. **Reconciliation**: Reconciles any discrepancies between Shopify and Appstle data 5. **Logging**: Logs all sync activities for audit trail **What Gets Synchronized:** **Customer Information:** - Customer ID and Shopify GraphQL ID - Customer name and email address - Customer tags and metadata - Customer acceptance status - Marketing preferences **Subscription Details:** - Active subscription contracts - Subscription statuses (ACTIVE, PAUSED, CANCELLED, EXPIRED) - Next billing dates - Billing interval and delivery interval - Subscription line items (products and variants) - Pricing and discounts **Payment Information:** - Associated payment methods - Payment instrument status - Billing address details **Delivery Information:** - Shipping address details - Delivery method and profile - Delivery preferences **Use Cases:** **1. Data Consistency:** - Resolve data discrepancies between Shopify and Appstle - Update customer information after changes in Shopify admin - Sync subscription modifications made directly in Shopify **2. Troubleshooting:** - Fix sync issues for specific customers - Recover from webhook delivery failures - Debug customer portal display issues **3. Migration & Integration:** - Initial data sync after app installation - Re-sync after system maintenance or updates - Integration testing and validation **4. Customer Support:** - Refresh customer data when assisting with support tickets - Verify latest subscription status during customer inquiries - Update data after manual changes in Shopify **When to Use This Endpoint:** **Recommended Scenarios:** - Customer reports incorrect subscription data in portal - After making manual changes to subscriptions in Shopify admin - When troubleshooting webhook sync failures - Before running bulk operations on customer subscriptions - During migration or data reconciliation processes **Avoid Using For:** - Real-time data refresh (webhooks handle this automatically) - Frequent polling (use webhooks instead to avoid rate limits) - Bulk syncs of many customers (use batch endpoints or scheduled jobs) **Sync Behavior:** **Synchronous Operation:** - Endpoint blocks until sync completes - Returns void on success - Throws exception on failure **Data Precedence:** - Shopify data is always treated as source of truth - Local Appstle data is overwritten with Shopify values - Custom Appstle-specific fields are preserved **Error Handling:** - Invalid customer ID: Returns 400 Bad Request - Customer not found in Shopify: Returns 404 Not Found - Shopify API errors: Returns 502 Bad Gateway - Rate limit exceeded: Returns 429 Too Many Requests **Important Considerations:** **Performance:** - Sync duration depends on number of subscriptions (typically 1-5 seconds) - May timeout for customers with 100+ active subscriptions - Consider using asynchronous sync for high-volume customers **Rate Limiting:** - Subject to Shopify GraphQL API rate limits - Frequent calls may exhaust rate limit budget - Use webhooks for real-time sync instead of polling this endpoint **Data Integrity:** - Always creates audit log entries for tracking - Preserves historical data and activity logs - Does not delete local-only data (e.g., custom notes) **Best Practices:** 1. **Use Sparingly**: Rely on webhooks for automatic sync; use this only when needed 2. **Check Logs**: Review activity logs after sync to verify changes 3. **Validate Results**: Query customer data after sync to confirm updates 4. **Handle Errors**: Implement retry logic with exponential backoff 5. **Monitor Rate Limits**: Track Shopify API usage to avoid hitting limits **Security Notes:** - Requires valid API key authentication via X-API-Key header - Customer ID is validated against shop ownership - Cannot sync customers from other shops - All sync operations are logged for audit compliance **Response:** - Returns HTTP 204 No Content on successful sync - No response body returned - Check activity logs for detailed sync results **Alternative Approaches:** If you need to: - **Sync multiple customers**: Use bulk sync endpoint or scheduled job - **Real-time updates**: Rely on webhook subscriptions - **Verify data without modifying**: Use GET endpoints to retrieve current data **Integration Workflow Example:** ``` 1. Customer contacts support about incorrect billing date 2. Support agent checks subscription in Appstle admin 3. Agent makes correction in Shopify admin 4. Agent calls sync endpoint for this customer 5. Sync retrieves latest data from Shopify 6. Appstle database is updated with corrected billing date 7. Customer portal now shows correct date 8. Support ticket resolved ``` **Authentication:** Requires valid X-API-Key header or api_key parameter (deprecated) # Get custom CSS for customer portal Source: https://developers.appstle.com/subscription-admin-api/customization/get-custom-css-for-customer-portal /subscription/admin-api-swagger.json get /api/external/v2/subscription-custom-csses/{id} Retrieves the custom CSS styling configuration for the customer portal. This endpoint returns all custom CSS rules that have been configured to customize the appearance, layout, and branding of the subscription customer portal. **What is Custom CSS?** Custom CSS allows merchants to fully customize the visual appearance of their customer portal beyond the basic theme settings. This enables complete brand alignment and creates a seamless experience that matches the merchant's main store design. **Custom CSS Capabilities:** - **Layout Customization**: - Modify page layouts and spacing - Adjust grid and flexbox configurations - Control responsive breakpoints - Customize navigation and sidebars - **Typography**: - Custom fonts and font families - Font sizes, weights, and line heights - Letter spacing and text transforms - Heading and paragraph styles - **Colors and Branding**: - Brand color palette application - Custom background colors and gradients - Button and link styling - Hover and focus states - Border colors and shadows - **Component Styling**: - Subscription card appearances - Form input styling - Button designs and interactions - Modal and dialog boxes - Navigation menus - Product images and thumbnails - **Advanced Features**: - CSS animations and transitions - Media queries for responsive design - Pseudo-elements and pseudo-classes - Custom icons using CSS - Transform and filter effects **CSS Structure:** The returned CSS includes: - Global styles for portal-wide consistency - Component-specific styles - Responsive design rules - Theme overrides - Custom animations - Print styles (optional) **Common CSS Selectors Available:** ```css /* Portal container */ .subscription-portal { } /* Subscription cards */ .subscription-card { } .subscription-card-header { } .subscription-card-body { } /* Buttons */ .btn-primary { } .btn-secondary { } .btn-cancel { } /* Forms */ .form-control { } .form-group { } .form-label { } /* Navigation */ .portal-nav { } .nav-item { } /* Product displays */ .product-item { } .product-image { } .product-title { } ``` **Use Cases:** - Apply custom branding to match main store design - Create unique visual experiences for different customer segments - Implement seasonal or promotional themes - Enhance mobile responsiveness - Add accessibility improvements (high contrast, larger fonts) - A/B test different portal designs - Integrate with design systems - Implement dark mode or theme switching **Important Notes:** - CSS is sanitized for security (XSS prevention) - Certain properties may be restricted for security reasons - External resources (fonts, images) must use HTTPS - CSS is cached for performance - changes may take a few minutes to propagate - Invalid CSS syntax is automatically filtered out - Some core portal elements have !important styles that cannot be overridden **Best Practices:** - Use specific selectors to avoid conflicts - Test across different browsers and devices - Keep CSS organized with comments - Use CSS variables for maintainability - Minify CSS for production performance - Consider accessibility in color choices (WCAG compliance) - Provide fallbacks for advanced CSS features **Security Considerations:** - CSS is sanitized to prevent code injection - External URLs are validated - JavaScript in CSS is blocked (e.g., expression(), behavior()) - Data URIs are validated for malicious content **Authentication:** Requires valid X-API-Key header # Get customer portal settings Source: https://developers.appstle.com/subscription-admin-api/customization/get-customer-portal-settings /subscription/admin-api-swagger.json get /api/external/v2/customer-portal-settings/{id} Retrieves the customer portal configuration and settings for the authenticated shop. The customer portal is the self-service interface where subscribers can manage their subscriptions, update payment methods, modify delivery addresses, and more. **What is the Customer Portal?** The customer portal is a dedicated web interface that allows your subscribers to manage their subscription accounts independently. This reduces support burden and improves customer experience by enabling self-service subscription management. **Settings Returned:** - **Display Configuration**: - Portal theme and branding settings - Custom colors and logo - Layout preferences - Custom CSS selectors - **Feature Toggles**: - Enable/disable subscription pausing - Enable/disable order skipping - Enable/disable product swapping - Enable/disable frequency changes - Enable/disable quantity modifications - Enable/disable address editing - Enable/disable payment method updates - Enable/disable subscription cancellation - **Subscription Management Options**: - Maximum pause duration allowed - Minimum subscription duration requirements - Skip limits per billing cycle - Product swap availability - One-time product add-ons - **Communication Settings**: - Email notification preferences - Custom portal messaging - Support contact information - Help text and instructions - **Access Control**: - Portal authentication method - Password requirements - Magic link settings - Session duration - **Advanced Options**: - Custom domain configuration - Redirect URLs - Webhook endpoints - Analytics tracking settings **Use Cases:** - Display customer portal with correct branding and theme - Determine which features are available to subscribers - Build custom portal interfaces using your settings - Sync portal configuration across systems - Validate subscription management capabilities - Configure third-party integrations **Important Notes:** - Settings are shop-specific and unique per merchant - Some features may be restricted based on subscription plan - Changes to settings are reflected immediately in the portal - Custom CSS must be valid and secure - Portal URL is typically: shop-domain.com/apps/subscriptions **Common Configuration Scenarios:** **1. Standard Self-Service Portal:** - Allow pausing (up to 3 months) - Allow skipping (max 2 consecutive orders) - Allow frequency changes - Allow quantity updates - Allow address editing - Enable payment method updates - Enable cancellation with feedback **2. Locked-Down Portal (Minimal Self-Service):** - Disable pausing - Disable skipping - Disable cancellation (require support contact) - Allow address editing only - Allow payment method updates only **3. Full-Service Portal (Maximum Flexibility):** - Enable all subscription management features - Allow unlimited pauses and skips - Enable product swapping - Enable one-time add-ons - Allow subscription splitting/merging - Custom branding and domain **Authentication:** Requires valid X-API-Key header # Regenerate subscription widget scripts Source: https://developers.appstle.com/subscription-admin-api/customization/regenerate-subscription-widget-scripts /subscription/admin-api-swagger.json get /api/external/v2/theme-settings/regenerate-scripts-for-shop Triggers regeneration and deployment of subscription widget JavaScript files for the authenticated shop. This endpoint rebuilds the widget scripts that power subscription functionality on your storefront and deploys them to CDN. **What Does This Endpoint Do?** This endpoint initiates an asynchronous process to regenerate and update the JavaScript files that enable subscription widgets on your store's theme. The widget scripts handle: - Subscription product selection and display - Subscription plan offerings on product pages - Frequency and delivery interval selectors - Subscription pricing display - Add-to-cart subscription functionality - Widget styling and customization **When to Use This Endpoint:** **1. After Widget Settings Changes:** - Modified widget appearance or styling - Changed subscription plan display options - Updated widget text or labels - Altered widget positioning or layout - Changed frequency options display **2. After Theme Customization:** - Installed a new theme - Updated existing theme - Made CSS customizations affecting widgets - Changed theme structure requiring widget updates **3. After Plan Configuration Changes:** - Added new subscription plans - Modified existing plan details - Changed plan pricing or discounts - Updated plan availability rules **4. Troubleshooting:** - Widget not displaying correctly on storefront - Subscription options showing outdated information - Widget functionality broken after theme changes - Script conflicts or errors on product pages - Cache issues preventing updates from showing **5. After App Updates:** - Appstle subscription app has been upgraded - New widget features have been released - Bug fixes requiring script updates **How It Works:** 1. **Initiation**: API call triggers the script regeneration process 2. **Compilation**: System compiles widget configuration, theme settings, and subscription plans into optimized JavaScript 3. **Deployment**: Generated scripts are uploaded to CDN for fast global delivery 4. **Cache Invalidation**: Old cached versions are invalidated 5. **Completion**: Updated scripts become available to your storefront (typically within 1-2 minutes) **What Gets Regenerated:** - **Widget JavaScript**: Core widget functionality and UI components - **Configuration Data**: Embedded shop-specific settings and plan information - **Styling Rules**: Custom CSS and theme-specific styles - **Initialization Code**: Auto-load and widget mounting logic - **Event Handlers**: Customer interaction and analytics tracking **Process Details:** - **Asynchronous**: The regeneration happens in the background - **Non-blocking**: Endpoint returns immediately (true on success) - **No downtime**: Existing widgets continue working during regeneration - **Automatic deployment**: Scripts are automatically deployed to CDN - **Versioning**: New script versions don't break existing functionality **Expected Behavior:** - Endpoint returns `true` immediately to confirm process started - Script regeneration completes in background (typically 30-90 seconds) - Updated scripts propagate to CDN (1-2 minutes) - Browser cache may need clearing to see changes immediately - Changes visible to customers after cache expiration (varies by browser) **Use Cases:** - **Automated Deployments**: Include in CI/CD pipeline after theme updates - **Widget Troubleshooting**: Force refresh when widget issues occur - **Configuration Sync**: Ensure widgets reflect latest settings after bulk changes - **Theme Migration**: Update scripts after moving to new theme - **Testing**: Regenerate scripts after making configuration changes in staging - **Maintenance**: Periodic regeneration to ensure optimal performance **Important Notes:** - Safe to call multiple times (idempotent operation) - No negative impact on existing subscriptions - Does not modify theme files directly - Scripts are hosted on CDN, not in your theme - Changes apply to all store pages using subscription widgets - Browser caching may delay visibility of changes to end users **Best Practices:** - Call this endpoint after making widget setting changes - Wait 2-3 minutes before testing changes on storefront - Clear browser cache when testing to see latest version - Use in test/staging environment before production - Avoid calling excessively (once per configuration change is sufficient) - Monitor widget functionality after regeneration **Troubleshooting:** If widgets still don't reflect changes after regeneration: 1. Wait 5 minutes for full CDN propagation 2. Clear browser cache and hard refresh (Ctrl+Shift+R / Cmd+Shift+R) 3. Check browser console for JavaScript errors 4. Verify theme has widget embed code installed 5. Confirm subscription plans are properly configured 6. Test in incognito/private browsing mode **Integration Workflows:** **Theme Update Workflow:** 1. Make theme changes 2. Update widget settings if needed 3. Call regenerate-scripts endpoint 4. Wait 2-3 minutes 5. Test widgets on storefront 6. Deploy to production **Bulk Configuration Update:** 1. Update multiple subscription plans 2. Modify widget appearance settings 3. Call regenerate-scripts once (not after each change) 4. Verify changes propagated correctly **Authentication:** Requires valid API key via api_key parameter or X-API-Key header # Bulk update subscription billing interval Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/bulk-update-subscription-billing-interval /subscription/admin-api-swagger.json put /api/external/v2/bulk-automations/billing-interval Updates the billing interval configuration for subscription contracts in bulk. **Endpoint:** `PUT /api/external/v2/bulk-automations/billing-interval` **Authentication:** - API Key (query parameter: api_key) **Query Parameters:** - `api_key` (required) – Your API key - `intervalCount` (required) – Billing interval frequency (e.g. 1, 2, 3) - `interval` (required) – Billing interval type (DAY, WEEK, MONTH, YEAR) - `suppressEmailNotification` (optional) – Suppress email notification to customer (true/false) - `allSubscriptions` (optional) – Apply to all subscriptions for the shop (true/false) **Request Body:** ```json { "subscriptionIds": "41982558492,41838280988" } ``` **What This Endpoint Does:** - Updates the billing interval (frequency + interval type) for provided subscription contracts - Creates a bulk automation task - Processes updates asynchronously in the background using AWS Step Functions **How It Works:** 1. Merchant provides billing interval details via query parameters 2. Subscription IDs are provided as a comma-separated string in the request body (ignored if allSubscriptions=true) 3. System validates shop ownership and ensures no bulk job is currently running 4. Bulk automation job is created 5. Billing interval is updated for each subscription asynchronously **Important Notes:** - Only one bulk automation per shop can run at a time - `interval` must match SellingPlanInterval enum values exactly (DAY, WEEK, MONTH, YEAR) - Processing is asynchronous - Previously generated orders are not modified - If `allSubscriptions=true`, provided subscriptionIds will be ignored - If `allSubscriptions=false`, subscriptionIds cannot be empty # Bulk update subscription delivery method Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/bulk-update-subscription-delivery-method /subscription/admin-api-swagger.json put /api/external/v2/bulk-automations/delivery-method Updates the delivery method configuration for subscription contracts in bulk. **Endpoint:** `PUT /api/external/v2/bulk-automations/delivery-method` **Authentication:** - API Key (query parameter: api_key) **Query Parameters:** - `api_key` (required) – Your API key - `deliveryMethodTitle` (required) – Delivery method title - `deliveryMethodCode` (required) – Delivery method code - `deliveryMethodPresentmentTitle` (required) – Customer-facing delivery method title - `deliveryMethodId` (required) – Shopify Delivery Method Definition GraphQL ID **Request Body:** ```json { "subscriptionIds": "29879894300" } ``` **What This Endpoint Does:** - Updates the delivery method details for the provided subscription contracts - Creates a bulk automation task - Processes updates asynchronously in the background **How It Works:** 1. Merchant provides delivery method details via query parameters 2. Subscription IDs are provided in request body 3. System validates shop ownership and active status 4. Bulk automation job is created 5. Delivery method is updated for each subscription **Important Notes:** - Only one bulk automation per shop can run at a time - deliveryMethodId must be in Shopify GraphQL ID format (e.g. gid://shopify/DeliveryMethodDefinition/...) - Processing is asynchronous - Previous generated orders are not modified # Bulk update subscription delivery price Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/bulk-update-subscription-delivery-price /subscription/admin-api-swagger.json put /api/external/v2/bulk-automations/delivery-price Updates the delivery price for subscription contracts in bulk. **Endpoint:** `PUT /api/external/v2/bulk-automations/delivery-price` **Authentication:** - API Key (query parameter: api_key) **Query Parameters:** - `api_key` (required) – Your API key - `deliveryPrice` (required) – New delivery price to be applied - `allSubscriptions` (optional) – Apply to all subscriptions for the shop (true/false) **Request Body:** ```json { "subscriptionIds": "28191031580" } ``` **What This Endpoint Does:** - Updates the delivery price for provided subscription contracts - Creates a bulk automation task - Processes updates asynchronously in the background using AWS Step Functions **How It Works:** 1. Merchant provides new delivery price via query parameter 2. Subscription IDs are provided as a comma-separated string in the request body (ignored if allSubscriptions=true) 3. System validates shop ownership and ensures no bulk job is currently running 4. Bulk automation job is created 5. Delivery price is updated for each subscription asynchronously **Important Notes:** - Only one bulk automation per shop can run at a time - Processing is asynchronous - Previously generated orders are not modified - If `allSubscriptions=true`, provided subscriptionIds will be ignored - If `allSubscriptions=false`, subscriptionIds cannot be empty # Create a new shipping/delivery profile Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/create-a-new-shippingdelivery-profile /subscription/admin-api-swagger.json post /api/external/v2/delivery-profiles/create-shipping-profile Creates a new shipping or delivery profile for subscription orders. This endpoint allows you to configure custom shipping rates, delivery methods, and shipping zones for your subscription products. Delivery profiles control how subscription products are shipped to customers. **Key Features:** - Define custom shipping rates per zone - Configure local delivery options - Set up local pickup methods - Manage shipping zones and countries - Assign profiles to specific products or variants **Delivery Method Types:** - **SHIPPING**: Standard shipping with carrier rates or custom rates - **LOCAL_DELIVERY**: Local delivery service within specific areas - **PICKUP**: Customer pickup from store locations **Important Notes:** - Each delivery profile must have a unique name - At least one delivery method is required - Shipping rates can be set per zone and per weight/price range - Profiles can be assigned to subscription products to control their shipping behavior **Authentication:** Requires valid X-API-Key header # Create a new shipping/delivery profile (V2) Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/create-a-new-shippingdelivery-profile-v2 /subscription/admin-api-swagger.json post /api/external/v2/delivery-profiles/v2/create-shipping-profile Creates a new shipping or delivery profile for subscription orders using the V2 request format. This version provides enhanced configuration options for delivery methods, zones, and pricing. **Enhancements in V2:** - Improved zone configuration - Better support for multiple delivery methods - Enhanced rate definition capabilities - Support for conditional delivery rates **Key Features:** - Define custom shipping rates per zone - Configure local delivery options - Set up local pickup methods - Manage shipping zones and countries - Assign profiles to specific products or variants **Authentication:** Requires valid X-API-Key header # Create a new shipping/delivery profile (V3) Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/create-a-new-shippingdelivery-profile-v3 /subscription/admin-api-swagger.json post /api/external/v2/delivery-profiles/v3/create-shipping-profile Creates a new shipping or delivery profile for subscription orders using the V3 request format. This is the latest version with the most comprehensive delivery profile configuration capabilities. **Enhancements in V3:** - Full support for all Shopify delivery profile features - Advanced rate calculation options - Support for weight-based and price-based rates - Enhanced zone and country management - Better handling of delivery conditions and rules **Key Features:** - Define complex shipping rate structures - Configure multiple delivery methods per zone - Set up conditional rates based on weight, price, or item count - Manage detailed shipping zones with province-level granularity - Full control over delivery profile settings **Authentication:** Requires valid X-API-Key header # Get cancellation management configuration Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/get-cancellation-management-configuration /subscription/admin-api-swagger.json get /api/external/v2/cancellation-managements/{id} Retrieves the cancellation management and retention settings for the authenticated shop. These settings control the subscription cancellation flow, retention strategies, and customer feedback collection when subscribers attempt to cancel their subscriptions. **What is Cancellation Management?** Cancellation management is a retention system that helps merchants reduce subscription churn by understanding why customers cancel and offering alternatives before they leave. It includes cancellation flows, feedback collection, retention offers (discounts, pauses, frequency changes), and automated win-back strategies. **Configuration Components:** **1. Cancellation Flow Settings:** - Enable/disable self-service cancellation in customer portal - Require customer to contact support for cancellation - Multi-step cancellation confirmation process - Cancellation cooldown period (prevent accidental cancellations) - Minimum subscription duration before cancellation allowed - Immediate vs. end-of-billing-cycle cancellation **2. Cancellation Reasons & Feedback:** - Predefined cancellation reasons list - Custom cancellation reason options - Required vs. optional feedback - Free-form text feedback field - Rating/satisfaction scale - Exit survey questions - Feedback sent to merchant email/dashboard **Common Cancellation Reasons:** - Too expensive / Can't afford - Don't use product enough / Have too much inventory - Product quality issues - Switching to competitor - Delivery issues / Shipping problems - Forgot to cancel earlier - Temporary pause needed (moving, travel, etc.) - Product doesn't meet expectations - Customer service issues - Other (with text field) **3. Retention Offers (Save Flow):** - Display retention offers before final cancellation - Offer discount (percentage or fixed amount) - Offer free product/gift with next order - Suggest subscription pause instead of cancel - Suggest frequency change (deliver less often) - Offer to skip next order - Offer to swap products - Personalized offers based on cancellation reason **Example Retention Logic:** ``` Reason: "Too expensive" → Offer: 20% discount for next 3 months → Offer: Switch to smaller/cheaper variant Reason: "Have too much inventory" → Offer: Pause for 1-3 months → Offer: Change from monthly to every 2 months → Offer: Skip next 2 deliveries Reason: "Product quality issues" → Offer: Free replacement on next order → Offer: Try different product variant → Route to customer support ``` **4. UI/UX Configuration:** - Cancellation button placement and styling - Warning messages and modal dialogs - Progress indicator for multi-step flow - Retention offer presentation (modal, inline, etc.) - Confirmation messages - Post-cancellation survey **5. Automation & Notifications:** - Send cancellation confirmation email - Notify merchant of cancellations - Trigger win-back email campaigns - Schedule feedback review - Alert for high cancellation rate - Integration with CRM/analytics tools **6. Metrics & Analytics:** - Track cancellation rate - Categorize cancellation reasons - Measure retention offer acceptance rate - Calculate customer lifetime value at cancellation - Identify churn patterns and trends - A/B test different retention strategies **7. Advanced Features:** - Conditional retention offers based on: - Customer lifetime value - Subscription duration - Product type - Cancellation history - Customer segment - Win-back campaigns for cancelled subscribers - Automatic re-engagement emails (30, 60, 90 days post-cancel) - Special offers to reactivate cancelled subscriptions **Cancellation Flow Examples:** **Basic Flow (No Retention):** 1. Customer clicks "Cancel Subscription" 2. Confirmation dialog: "Are you sure?" 3. Optional: Select cancellation reason 4. Subscription cancelled immediately or at period end 5. Confirmation email sent **Advanced Retention Flow:** 1. Customer clicks "Cancel Subscription" 2. "Before you go..." - Select cancellation reason 3. Display personalized retention offer based on reason 4. Customer chooses: Accept offer OR Continue to cancel 5. If continue: Additional offers or final confirmation 6. If still cancelling: Exit survey 7. Cancellation processed 8. Thank you message + feedback confirmation 9. Follow-up email campaign **Use Cases:** - Configure cancellation flow in customer portal - Customize retention offers and messaging - Analyze cancellation patterns and trends - Build custom cancellation interfaces - Integrate with customer support tools - Generate churn reports and analytics - A/B test different retention strategies - Automate win-back campaigns **Important Notes:** - Always comply with consumer protection laws regarding cancellation - Make cancellation process clear and accessible (FTC guidelines) - Don't make cancellation unnecessarily difficult - Respect customer's decision to cancel - Use retention offers ethically (not dark patterns) - Store cancellation feedback for product improvement - GDPR/privacy compliance for feedback data **Best Practices:** - Keep cancellation process simple (2-3 steps max) - Offer genuine value in retention offers - Personalize offers based on cancellation reason - Make pause/skip options prominent - Use cancellation feedback to improve product/service - Set up automated win-back campaigns - Monitor cancellation rate and take action on trends - Train customer support on retention strategies - Test different retention messaging and offers - Honor cancellations promptly and professionally **Legal & Compliance:** - FTC regulations require easy cancellation (Click-to-Cancel rule) - California automatic renewal law compliance - European consumer protection directives - Clear disclosure of cancellation terms - Process cancellations within legal timeframes - Provide confirmation of cancellation **Authentication:** Requires valid X-API-Key header # Get/Search activity logs Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/getsearch-activity-logs /subscription/admin-api-swagger.json get /api/external/v2/activity-logs Retrieves activity logs for all subscription-related events and changes in the authenticated shop. Activity logs provide a comprehensive audit trail of all subscription activities, changes, and system events. **What are Activity Logs?** Activity logs are detailed records of every action, event, and change that occurs within your subscription system. They capture who made the change, when it happened, what was changed, and the result of the operation. This provides full traceability and audit capabilities for compliance, debugging, and customer support. **Log Information Captured:** - **Entity Information**: - Entity ID: The specific subscription, billing attempt, or resource affected - Entity Type: Type of resource (subscription, billing attempt, plan, settings, etc.) - Event Type: Specific action performed (see Event Types below) - Event Source: Origin of the event (customer portal, merchant portal, API, system, etc.) - **Event Details**: - Status: SUCCESS, FAILURE, or INFO - Timestamp: When the event occurred - Activity By: User or system that triggered the event - Client IP: IP address of the requester (if applicable) - Additional Info: Detailed JSON payload with before/after values and context **Entity Types:** - `SUBSCRIPTION_BILLING_ATTEMPT` - Billing and payment attempt events - `SUBSCRIPTION_CONTRACT_DETAILS` - Subscription contract modifications - `SUBSCRIPTION_GROUP_PLAN` - Subscription plan changes - `SHIPPING_PROFILE` - Delivery profile and shipping updates - `CANCELLATION_MANAGEMENT` - Subscription cancellation events - `DUNNING_MANAGEMENT` - Payment retry and dunning activities - `PRODUCT_SWAP` - Product replacement events - `FREQUENCY_SWAP` - Billing/delivery frequency changes - `EMAIL_TEMPLATE` - Email template modifications - `SHOP_INFO` - Shop settings and configuration changes - `SUBSCRIPTION_WIDGET_SETTINGS` - Widget configuration updates - `CUSTOMER_PORTAL_SETTINGS` - Portal settings modifications - `SUBSCRIPTION_BUNDLING` - Bundle and bundling rule changes **Event Sources:** - `CUSTOMER_PORTAL` - Changes made by subscribers through the customer portal - `MERCHANT_PORTAL` - Changes made by merchants in the admin panel - `MERCHANT_EXTERNAL_API` - Changes made via API by merchants or integrations - `SHOPIFY_EVENT` - Events triggered by Shopify webhooks - `SHOPIFY_FLOW` - Events triggered by Shopify Flow automations - `SYSTEM_EVENT` - Automated system events (scheduled tasks, sync operations) - `MERCHANT_PORTAL_BULK_AUTOMATION` - Bulk operations initiated from admin panel **Common Event Types:** - **Subscription Changes**: CONTRACT_PAUSED, CONTRACT_ACTIVATED, CONTRACT_CANCELLED, CONTRACT_CREATED - **Product Changes**: PRODUCT_ADD, PRODUCT_REMOVE, PRODUCT_REPLACE, PRODUCT_QUANTITY_CHANGE, PRODUCT_PRICE_CHANGE - **Billing Changes**: NEXT_BILLING_DATE_CHANGE, BILLING_INTERVAL_CHANGE, BILLING_ATTEMPT_TRIGGERED, BILLING_ATTEMPT_SKIPPED - **Delivery Changes**: DELIVERY_INTERVAL_CHANGE, SHIPPING_ADDRESS_CHANGE, DELIVERY_METHOD_UPDATED - **Payment Changes**: PAYMENT_METHOD_UPDATED, SWITCH_PAYMENT_METHODS - **Email Events**: SEND_UPCOMING_ORDER_EMAIL, SEND_TRANSACTION_FAILED_EMAIL, SEND_SUBSCRIPTION_CREATED_EMAIL - **Discount Events**: DISCOUNT_APPLIED, DISCOUNT_REMOVED, PRICING_POLICY_DISCOUNT_APPLIED - **One-Time Products**: ONE_TIME_PURCHASE_PRODUCT_ADDED, ONE_TIME_PURCHASE_PRODUCT_REMOVED **Query Parameters and Filtering:** You can filter activity logs using query parameters. All filters support standard operations: - `equals` - Exact match - `in` - Match any value in list - `greaterThan`, `lessThan` - Date range filtering - `contains` - Partial text match **Example Filter Queries:** 1. **Get all failed billing attempts:** `?entityType.equals=SUBSCRIPTION_BILLING_ATTEMPT&eventType.equals=BILLING_ATTEMPT_TRIGGERED&status.equals=FAILURE` 2. **Get all customer portal changes:** `?eventSource.equals=CUSTOMER_PORTAL` 3. **Get logs for a specific subscription:** `?entityId.equals=123456&entityType.equals=SUBSCRIPTION_CONTRACT_DETAILS` 4. **Get logs within date range:** `?createAt.greaterThan=2024-01-01T00:00:00Z&createAt.lessThan=2024-01-31T23:59:59Z` 5. **Get all cancellation events:** `?eventType.equals=CONTRACT_CANCELLED` 6. **Get all API-triggered changes:** `?eventSource.equals=MERCHANT_EXTERNAL_API` **Pagination:** Results are paginated. Use standard Spring Data pagination parameters: - `page` - Page number (zero-indexed, default: 0) - `size` - Page size (default: 20, max: 100) - `sort` - Sort criteria (e.g., `createAt,desc` or `id,asc`) Example: `?page=0&size=50&sort=createAt,desc` **Use Cases:** - **Audit and Compliance**: Track all changes for regulatory compliance and internal auditing - **Customer Support**: Review subscription history to troubleshoot customer issues - **Debugging**: Investigate failed operations and system errors - **Analytics**: Analyze customer behavior and subscription lifecycle patterns - **Reporting**: Generate reports on subscription changes, cancellations, and modifications - **Integration Monitoring**: Track API usage and automated workflow executions - **Security**: Monitor for unusual activity patterns or unauthorized changes **Important Notes:** - Activity logs are immutable and cannot be modified or deleted via API - Logs are retained according to your subscription plan's retention policy - The `additionalInfo` field contains detailed JSON with event-specific context - For large exports, consider using pagination and filtering to reduce data transfer - Logs are scoped to your shop - you can only access logs for your own subscriptions - System events may not have an `activityBy` value as they're automated **Response Headers:** - `X-Total-Count` - Total number of matching records - `Link` - Pagination links (first, last, next, prev) **Authentication:** Requires valid api_key query parameter with appropriate permissions # Hide subscriptions in bulk Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/hide-subscriptions-in-bulk /subscription/admin-api-swagger.json post /api/external/v2/bulk-automations/hide-subscriptions Hides multiple subscription contracts from customer view in bulk. This operation allows merchants to quickly hide subscriptions from appearing in the customer portal without canceling or deleting them. **What Does 'Hide' Mean?** Hiding a subscription makes it invisible to customers in their customer portal while keeping the subscription data intact. The subscription is not deleted or canceled - it's simply hidden from the customer's view. This is useful for: - Temporarily removing subscriptions from customer access - Managing test or dummy subscriptions - Handling subscription disputes or issues - Preparing subscriptions for migration or cleanup **Key Features:** - **Bulk Operation**: Process multiple subscriptions in a single request - **Non-Destructive**: Subscriptions are hidden, not deleted - **Reversible**: Hidden subscriptions can be unhidden later - **Asynchronous Processing**: Large batches are processed in the background - **Conflict Detection**: Prevents multiple simultaneous bulk operations **Operation Modes:** 1. **Specific Subscriptions**: Provide a list of subscription contract IDs to hide 2. **All Subscriptions**: Set allSubscriptions=true to hide all active subscriptions (use with caution) **How It Works:** 1. Submit a request with subscription IDs or allSubscriptions flag 2. System validates the subscription IDs belong to your shop 3. A bulk automation task is created and queued 4. Each subscription is marked as hidden 5. Subscriptions disappear from customer portal immediately 6. Subscriptions remain in merchant admin for management **Request Body Structure:** ```json { "subscriptionIds": [ "gid://shopify/SubscriptionContract/123456", "gid://shopify/SubscriptionContract/123457", "gid://shopify/SubscriptionContract/123458" ] } ``` **Use Cases:** - **Subscription Cleanup**: Hide test or duplicate subscriptions created during setup - **Customer Service**: Temporarily hide problematic subscriptions while resolving issues - **Migration Preparation**: Hide old subscriptions before migrating to new plans - **Dispute Management**: Hide subscriptions involved in billing disputes - **Seasonal Management**: Hide seasonal subscriptions during off-season - **Batch Processing**: Clean up subscriptions that meet certain criteria **Important Notes:** - Only one bulk operation can run at a time per shop - If a bulk operation is already in progress, the request will fail with 400 error - Subscription IDs must be in Shopify GraphQL ID format (gid://shopify/SubscriptionContract/xxxxx) - Hidden subscriptions stop appearing in customer portal but remain active for billing - You can unhide subscriptions later through the admin interface - Using allSubscriptions=true will hide ALL subscriptions - use with extreme caution **Processing Time:** - Small batches (<100): Usually complete within seconds - Medium batches (100-1000): May take 1-2 minutes - Large batches (>1000): May take several minutes to hours - Progress can be tracked through the bulk automation status endpoint **Best Practices:** - Always specify exact subscription IDs rather than using allSubscriptions=true - Test with a small batch first before processing large numbers - Keep track of hidden subscription IDs for future reference - Document the reason for hiding subscriptions for audit purposes - Monitor bulk operation status to ensure completion - Consider notifying customers before hiding their subscriptions **Workflow Example:** 1. Identify subscriptions to hide (e.g., test subscriptions with specific tags) 2. Extract their subscription contract IDs 3. Call this endpoint with the list of IDs 4. Verify 204 No Content response indicating successful queue 5. Monitor processing status through admin or status endpoint 6. Confirm subscriptions are hidden from customer portal **Error Scenarios:** - Another bulk operation is running: 400 error with message about operation in progress - Invalid subscription IDs: Silently skips invalid IDs, processes valid ones - Unauthorized subscription access: Only subscriptions belonging to your shop are processed - Empty subscription list: Operation completes successfully with no action **Authentication:** Requires valid api_key parameter (X-API-Key header support coming soon) # Replace products in subscriptions in bulk Source: https://developers.appstle.com/subscription-admin-api/operations-&-settings/replace-products-in-subscriptions-in-bulk /subscription/admin-api-swagger.json post /api/external/v2/bulk-automations/replace-product Replaces old product variants with new product variants across multiple subscription contracts in bulk. This powerful operation allows merchants to update products in active subscriptions when products are discontinued, reformulated, or repackaged. **What is Product Replacement?** Product replacement updates the products in active subscriptions by swapping out old variant IDs with new variant IDs. This is commonly needed when: - Products are discontinued and replaced with new versions - Product packaging changes (size, quantity) - Product reformulations or recipe updates - SKU consolidation or reorganization - Seasonal product variations - Price structure changes **Key Features:** - **Bulk Operation**: Update thousands of subscriptions simultaneously - **Multi-Variant Support**: Replace multiple old variants with new ones in single request - **Flexible Mapping**: One-to-one, many-to-one, or one-to-many variant replacements - **Selective or Universal**: Target specific subscriptions or all subscriptions - **Asynchronous Processing**: Large batches processed in background - **Price Preservation Options**: Maintain existing subscription pricing or update to new prices - **Activity Logging**: All replacements are logged for audit trail **Operation Modes:** 1. **Specific Subscriptions**: Provide subscription contract IDs to update only those subscriptions 2. **All Subscriptions**: Set allSubscriptions=true to replace products in ALL active subscriptions containing the old variants **How It Works:** 1. Identify old variant IDs that need to be replaced 2. Identify new variant IDs that will replace them 3. Optionally specify which subscriptions to update (or use allSubscriptions=true) 4. Submit the bulk replacement request 5. System validates all variant IDs exist and are accessible 6. Bulk automation task is created and queued 7. Each subscription is updated with new variants 8. Customers receive updated subscription details 9. Next orders will include the new products **Variant ID Mapping:** The replacement supports flexible mapping between old and new variants: **One-to-One Replacement:** ```json { "oldVariantIds": [111111], "newVariantIds": [222222] } ``` Old variant 111111 is replaced with new variant 222222 **Multiple Variants Replacement:** ```json { "oldVariantIds": [111111, 333333, 555555], "newVariantIds": [222222, 444444, 666666] } ``` Each old variant is replaced with its corresponding new variant (by position) **Request Structure:** ```json { "subscriptionIds": [ "gid://shopify/SubscriptionContract/123456", "gid://shopify/SubscriptionContract/123457" ] } ``` **Query Parameters:** - `api_key` (required): Your API authentication key - `allSubscriptions` (optional): Set to true to update all subscriptions, false/omit to update only specified IDs - `newVariantIds` (required): Comma-separated list of new variant IDs (e.g., 222222,444444,666666) - `oldVariantIds` (required): Comma-separated list of old variant IDs to replace (e.g., 111111,333333,555555) **Use Cases:** **1. Product Discontinuation:** When a product is being discontinued: - Identify replacement product - Map old variant IDs to new variant IDs - Update all subscriptions containing the old product - Notify customers of the change **2. Packaging Updates:** When product packaging changes (e.g., 10oz to 12oz): - Create new variant for new package size - Replace old variant across subscriptions - Adjust pricing if needed **3. Product Reformulation:** When product recipes or formulas change: - Create new product variant for reformulated version - Bulk replace old formula with new formula - Maintain customer subscription frequency and pricing **4. SKU Consolidation:** When consolidating multiple variants into a single SKU: - Map multiple old variant IDs to single new variant ID - Update all affected subscriptions - Simplify inventory management **5. Seasonal Product Rotation:** For seasonal subscription boxes: - Replace summer variants with fall variants - Update all active seasonal subscriptions - Maintain subscription continuity **Important Considerations:** - **Pricing Impact**: New variants may have different prices - verify pricing strategy - **Inventory Levels**: Ensure adequate inventory for new variants - **Customer Communication**: Consider notifying customers before replacement - **Variant Compatibility**: New variants should be appropriate replacements - **One Operation at a Time**: Only one bulk operation can run per shop simultaneously - **Irreversible**: Product replacements cannot be automatically undone (must be manually reversed) - **Subscription Contract IDs**: Must use Shopify GraphQL ID format **Processing Time:** - Small batches (<100 subscriptions): Seconds to minutes - Medium batches (100-1000): Minutes - Large batches (>1000): Minutes to hours - Processing time depends on number of subscriptions and line items **Best Practices:** 1. **Test First**: Test with a small subset of subscriptions before bulk operation 2. **Verify Variants**: Confirm all variant IDs are correct and products are active 3. **Check Inventory**: Ensure sufficient stock of new variants 4. **Customer Communication**: Notify affected customers about product changes 5. **Price Review**: Review and confirm pricing for new variants 6. **Backup Data**: Export subscription data before making bulk changes 7. **Monitor Progress**: Track bulk operation status to completion 8. **Audit Trail**: Document reason for replacement for future reference **Error Scenarios:** - Another bulk operation running: 400 error - Invalid variant IDs: Operation may fail or skip invalid variants - Mismatched array lengths: Ensure oldVariantIds and newVariantIds have same count - Product not found: Variants must exist in your Shopify store - Unauthorized access: Can only modify subscriptions belonging to your shop **Customer Impact:** - Next subscription order will contain new products - Previous orders are not affected - Subscription price may change if new variant has different price - Customer portal reflects the new product immediately - Subscription frequency and schedule remain unchanged **Authentication:** Requires valid api_key parameter (X-API-Key header support coming soon) # Create a new product swap automation Source: https://developers.appstle.com/subscription-admin-api/product-catalog/create-a-new-product-swap-automation /subscription/admin-api-swagger.json post /api/external/v2/product-swaps Creates a new product swap automation rule for the authenticated shop. Product swaps automatically replace products in subscription orders based on billing cycles or recurring schedules. **Required Fields:** - **shop**: Your Shopify shop domain - **sourceVariants**: JSON string of variants to swap FROM (format: [{"id":12345,"quantity":2}]) - **destinationVariants**: JSON string of variants to swap TO (format: [{"id":67890,"quantity":2}]) - **name**: Descriptive name for this swap rule **Optional Fields:** - **forBillingCycle**: Specific cycle number when swap occurs (e.g., 4 = swap at 4th order). Leave null for recurring swaps. - **checkForEveryRecurringOrder**: If true, applies swap to every order (overrides forBillingCycle) - **updatedFirstOrder**: If true, can affect the first subscription order - **changeNextOrderDateBy**: Days to adjust next order date after swap (positive or negative integer) - **discountCarryForward**: How to handle discounts - NONE, PERCENTAGE, FIXED_AMOUNT, or PRICE - **carryDiscountForward**: Legacy boolean field (deprecated, use discountCarryForward instead) - **stopSwapEmails**: If true, suppresses customer notification emails about the swap - **ruleSequence**: Priority order when multiple swaps apply (lower number = higher priority) **Variant JSON Format:** The API enriches your variant data with product details from Shopify. You only need to provide: ```json [{"id": 12345, "quantity": 2}] ``` The response will include enriched data: displayName, imageSrc, productTitle, etc. **Authentication:** Requires valid X-API-Key header # Delete a product swap automation Source: https://developers.appstle.com/subscription-admin-api/product-catalog/delete-a-product-swap-automation /subscription/admin-api-swagger.json delete /api/external/v2/product-swaps/{id} Permanently deletes a product swap automation configuration. Once deleted, the swap will no longer be applied to any subscription orders. Existing subscriptions that have already had products swapped are not affected. **Deletion Behavior:** - Permanently removes the swap automation - Does not reverse past swaps that have already occurred - Future orders will not have this swap applied - Cannot be undone - swap configuration must be recreated if needed **Impact on Subscriptions:** - **Active Subscriptions**: Will continue with current products (no automatic reversion) - **Future Orders**: Swap will not be applied - **Past Orders**: Already swapped products remain unchanged - **Queued Swaps**: Pending swaps for this automation are cancelled **When to Delete:** - Discontinuing a seasonal product rotation - Removing outdated swap rules - Cleaning up test or experimental swaps - Product lines being discontinued - Correcting misconfigured swaps **Important Notes:** - This operation is permanent and cannot be undone - Consider deactivating instead of deleting if you might reuse the configuration - Activity logs for past swaps are retained - Customers are not automatically notified of swap deletion **Best Practices:** - Review affected subscriptions before deletion - Consider communicating changes to affected customers - Export swap configuration if you might need it later - Use deactivation for temporary pauses instead of deletion **Authentication:** Requires valid X-API-Key header # Get a specific product swap by ID Source: https://developers.appstle.com/subscription-admin-api/product-catalog/get-a-specific-product-swap-by-id /subscription/admin-api-swagger.json get /api/external/v2/product-swaps/{id} Retrieves detailed information about a specific product swap automation configuration. This endpoint returns complete details about the source products, destination products, swap triggers, and all associated settings. **Response Details:** - Complete swap configuration - Source product variants with images and quantities - Destination product variants with images and quantities - Billing cycle triggers and conditions - Discount carry-forward settings - Email notification preferences - Swap status and history **Source and Destination Variants:** Both source and destination variants are returned as JSON strings containing: - Variant ID - Display name/title - Product image URL - Quantity to swap - Product metadata **Swap Timing:** - **forBillingCycle**: If set, swap occurs at this specific cycle number - **checkForEveryRecurringOrder**: If true, swap happens on every order - **updatedFirstOrder**: If true, can affect the initial subscription order **Discount Handling:** - **NONE**: No discount carried forward - **PERCENTAGE**: Percentage discount is maintained - **FIXED_AMOUNT**: Fixed amount discount is maintained - **PRICE**: Specific price point is maintained **Authentication:** Requires valid X-API-Key header # Get all product swaps for a shop Source: https://developers.appstle.com/subscription-admin-api/product-catalog/get-all-product-swaps-for-a-shop /subscription/admin-api-swagger.json get /api/external/v2/product-swaps Retrieves all configured product swap automations for the authenticated shop. Product swaps allow automatic replacement of products in subscription orders based on billing cycles or recurring schedules. **What are Product Swaps?** Product swaps enable merchants to automatically replace products in subscription orders at specific billing cycles. This is useful for seasonal products, progression paths (e.g., beginner → intermediate → advanced), or variety subscriptions. **Key Features:** - View all configured swap automations - See source and destination product mappings - Check which billing cycles trigger swaps - Identify recurring vs one-time swaps - Review discount carry-forward settings **Swap Configuration Types:** - **One-Time Swap**: Occurs at a specific billing cycle - **Recurring Swap**: Occurs at every order - **Conditional Swap**: Based on billing cycle number **Use Cases:** - Seasonal product rotations (summer → fall → winter products) - Subscription progression (trial → full product) - Variety boxes with automatic product rotation - Product lifecycle management in subscriptions - A/B testing different products in subscriptions **Authentication:** Requires valid X-API-Key header # Get product swap variant groups for a contract Source: https://developers.appstle.com/subscription-admin-api/product-catalog/get-product-swap-variant-groups-for-a-contract /subscription/admin-api-swagger.json post /api/external/v2/product-swaps-by-variant-groups/{contractId} Retrieves product swap variant groups for the next 10 billing cycles of a specific subscription contract. This endpoint calculates and returns which products will be swapped to in future billing cycles based on configured swap automations. **Response Structure:** Returns a 2D array (`List>`): - **Outer array**: Represents the next 10 billing cycles - **Inner arrays**: Contains the variant(s) that will be swapped to for that specific cycle - **Index 0**: Variants for the next (upcoming) billing cycle - **Index 1**: Variants for the cycle after that - And so on for the next 10 cycles **Variant Details:** Each variant object includes: - **variantId**: Shopify variant ID - **quantity**: Number of units to swap - **title**: Variant title (e.g., "Medium Roast - 12oz") - **image**: Product/variant image URL - **productTitle**: Full product name - **productId**: Shopify product GID - **variantTitle**: Full display name combining product and variant titles - **swapId**: ID of the swap automation rule that triggered this swap **How It Works:** 1. Takes the current products in the subscription 2. Applies configured swap automations for the next 10 cycles 3. Calculates which products will be swapped based on: - Billing cycle number - Swap rule configurations (forBillingCycle, checkForEveryRecurringOrder) - Rule sequence/priority 4. Returns the projected product lineup for each cycle **Use Cases:** - Preview upcoming product swaps in customer portals - Show customers their subscription product timeline - Build interactive swap calendars - Display "what you'll receive" for future orders - Debug and verify swap automation configurations **Important Notes:** - Returns 10 cycles even if no swaps are configured (returns current products) - Multiple variants in an inner array means multiple products will be in that order - Empty inner arrays indicate no products for that cycle (rare edge case) - The swapId can be used to trace which automation rule triggered the swap **Authentication:** Requires valid X-API-Key header # Update an existing product swap automation Source: https://developers.appstle.com/subscription-admin-api/product-catalog/update-an-existing-product-swap-automation /subscription/admin-api-swagger.json put /api/external/v2/product-swaps Updates an existing product swap automation rule for the authenticated shop. Use this endpoint to modify swap configurations, change source/destination products, adjust timing, or update discount handling. **Required Fields:** - **id**: The product swap ID to update - **shop**: Your Shopify shop domain (must match authenticated shop) **Updatable Fields:** - **sourceVariants**: JSON string of variants to swap FROM - **destinationVariants**: JSON string of variants to swap TO - **name**: Descriptive name for this swap rule - **forBillingCycle**: Specific cycle number when swap occurs (null for recurring) - **checkForEveryRecurringOrder**: If true, applies swap to every order - **updatedFirstOrder**: If true, can affect the first subscription order - **changeNextOrderDateBy**: Days to adjust next order date after swap - **discountCarryForward**: How to handle discounts (NONE, PERCENTAGE, FIXED_AMOUNT, PRICE) - **carryDiscountForward**: Legacy boolean field (use discountCarryForward instead) - **stopSwapEmails**: If true, suppresses customer notification emails - **ruleSequence**: Priority order (lower number = higher priority) **Impact of Updates:** - Changes apply to future orders immediately - Past orders and already-swapped products are not affected - If changing forBillingCycle, recalculates which subscriptions are affected - Variant data is automatically enriched from Shopify on update **Authentication:** Requires valid X-API-Key header # Add custom discount to subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/add-custom-discount-to-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-add-discount Creates and applies a custom manual discount to an existing subscription contract. This powerful endpoint supports multiple discount types and configurations for flexible pricing strategies. **Discount Types:** - **PERCENTAGE**: Percentage off the order (e.g., 10% off) - **FIXED**: Fixed amount off the order (e.g., $10 off) - **PRICE**: Override price (sets total to specific amount) **Key Features:** - Custom discount titles for easy identification - Cycle limits for time-bound promotions - Per-item or subtotal application options - Stacks with other discounts per Shopify rules - Automatic shipping price recalculation - Activity logging for audit trails **Application Methods:** - **Subtotal Discount** (appliesOnEachItem=false): Applied to order total after line item calculations - **Per-Item Discount** (appliesOnEachItem=true): Applied to each line item individually **Cycle Limits:** - Set recurringCycleLimit to apply discount for specific number of orders - Leave null for unlimited duration - Useful for '3 months at 20% off' promotions **Retention Campaigns:** This endpoint integrates with cancellation retention workflows: - Can trigger merchant notifications - Tracks discount usage for retention analytics - Helps prevent churn with targeted offers **Important Notes:** - Discounts apply to future orders only - Cannot modify discounts once applied (must remove and re-add) - Some discount combinations may not be allowed by Shopify - Price overrides should be used carefully **Authentication:** Requires valid X-API-Key header # Apply discount code to subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/apply-discount-code-to-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-apply-discount Applies a Shopify discount code to an existing subscription contract. The discount will be applied to future orders generated by this subscription. **Key Features:** - Validates discount code through Shopify's discount system - Prevents duplicate discount code applications - Uses database locking to prevent race conditions - Automatically recalculates shipping prices if needed - Creates audit trail through activity logs - Returns updated subscription with discount details **Discount Code Validation:** - Must be an active Shopify discount code in the store - Must be applicable to subscription orders - Cannot be applied if already active on the subscription - Subject to Shopify's discount rules and restrictions **Customer Portal Restrictions:** When called from customer portal context: - If 'enableAllowOnlyOneDiscountCode' is enabled, customers cannot apply additional codes - This restriction ensures single discount policy enforcement - External API calls bypass this restriction **Concurrency Protection:** - Uses database-level locking on the subscription contract - Prevents simultaneous discount applications - Ensures data consistency in high-traffic scenarios **Post-Application Effects:** - Discount applies to all future orders from the subscription - May trigger shipping price recalculation - Creates 'DISCOUNT_APPLIED' activity log entry - Updates subscription's discount collection **Important Notes:** - Discount codes are case-sensitive - Invalid or expired codes will return appropriate errors - Discounts stack according to Shopify's combination rules - Some discounts may not be compatible with subscriptions **Authentication:** Requires valid X-API-Key header # Associate external payment gateway customer with Shopify customer Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/associate-external-payment-gateway-customer-with-shopify-customer /subscription/admin-api-swagger.json get /api/external/v2/associate-shopify-customer-to-external-payment-gateways Links a customer's payment profile from an external payment gateway (Stripe, Braintree, PayPal, Authorize.Net) to their Shopify customer account. This enables Shopify subscription billing to charge payment methods stored in external gateways, which is essential for migrating existing subscriptions or integrating with legacy payment systems. **What This Endpoint Does:** Creates a connection between a Shopify customer and their corresponding customer profile in an external payment gateway. This allows Shopify's subscription system to send billing requests to the external gateway using the stored payment credentials, effectively enabling subscriptions to bill through payment methods managed outside of Shopify. **Supported Payment Gateways:** **1. Stripe** (`paymentGateway=stripe`): - Requires: `customerProfileId` (Stripe customer ID, e.g., `cus_xxxxx`) - Requires: `paymentProfileId` (Stripe payment method ID, e.g., `pm_xxxxx` or card ID) - Links Shopify customer to Stripe customer and stored card/payment method **2. Braintree** (`paymentGateway=braintree`): - Requires: `customerProfileId` (Braintree customer ID) - Requires: `paymentProfileId` (Braintree payment method token) - Supports credit cards and PayPal accounts stored in Braintree vault **3. PayPal** (`paymentGateway=paypal`): - Requires: `paymentProfileId` (PayPal billing agreement ID) - Optional: `customerProfileId` (not required for PayPal) - Links Shopify customer to PayPal billing agreement **4. Authorize.Net** (`paymentGateway=authorize_net`): - Requires: `customerProfileId` (Authorize.Net customer profile ID) - Requires: `paymentProfileId` (Authorize.Net payment profile ID) - Links to Customer Information Manager (CIM) profiles **Request Parameters:** **Required Parameters:** - `paymentGateway` (string): Gateway name - `stripe`, `braintree`, `paypal`, or `authorize_net` - `paymentProfileId` (string): Payment method/card ID in the external gateway - `customerId` OR `email` (one required): Shopify customer identifier **Optional Parameters:** - `customerProfileId` (string): Customer ID in external gateway (required for Stripe, Braintree, Authorize.Net; optional for PayPal) **Customer Identification:** **By Customer ID (recommended):** - Provide Shopify `customerId` if known - Fastest and most reliable method - Avoids ambiguity with duplicate emails **By Email:** - Provide customer `email` if customer ID unknown - System looks up Shopify customer by email - If exactly one match: Uses that customer - If multiple matches: Returns error (ambiguous) - If no matches: Creates new Shopify customer with that email **Use Cases:** **1. Subscription Migration:** - Migrating subscriptions from legacy platform to Shopify - Preserving existing payment methods during migration - Avoiding customer disruption by not requiring payment re-entry - Maintaining payment history and customer profiles **2. Payment Gateway Integration:** - Using external payment processor for compliance reasons - Leveraging existing payment gateway relationships - Maintaining payment data in external PCI-compliant vault - Integrating with enterprise payment infrastructure **3. Multi-Platform Sync:** - Syncing payment methods from other sales channels - Centralizing payment methods across platforms - Enabling subscriptions for existing customer base **4. Custom Checkout Flows:** - Building custom subscription checkout with external gateway - Collecting payment via external gateway, then linking to Shopify - Supporting payment methods not natively available in Shopify **Process Flow:** 1. **Customer Lookup**: - If `customerId` provided: Use directly - If `email` provided: Search for existing Shopify customer - If email not found: Create new Shopify customer 2. **Validation**: - Validate payment gateway name - Check required parameters for selected gateway - Verify customer belongs to authenticated shop 3. **Gateway Association**: - Call Shopify GraphQL mutation for gateway-specific association - Link customer profile ID and payment profile ID - Store association in Shopify's payment instrument vault 4. **Response**: - Return Shopify's raw GraphQL response - Response includes payment instrument ID for future use - Contains success/error details from Shopify API **Example Requests:** **Stripe Example:** ``` GET /api/external/v2/associate-shopify-customer-to-external-payment-gateways? paymentGateway=stripe& customerId=12345& customerProfileId=cus_ABC123& paymentProfileId=pm_XYZ789 ``` **Braintree with Email:** ``` GET /api/external/v2/associate-shopify-customer-to-external-payment-gateways? paymentGateway=braintree& email=customer@example.com& customerProfileId=bt_customer_123& paymentProfileId=bt_card_token_456 ``` **PayPal Example:** ``` GET /api/external/v2/associate-shopify-customer-to-external-payment-gateways? paymentGateway=paypal& customerId=12345& paymentProfileId=B-12345BILLING67890 ``` **Important Considerations:** **Gateway Setup:** - External payment gateway must be installed and configured in Shopify - Gateway must be activated for subscription billing - Appropriate credentials and API keys must be configured **Data Validation:** - Payment profile IDs are not validated against external gateway - Invalid IDs will be accepted but will fail during actual billing - Test associations before migrating production subscriptions **Customer Creation:** - If customer doesn't exist and email provided, new customer is created - Created customers have minimal information (just email initially) - Consider adding additional customer details after creation **Security:** - This endpoint does NOT store or handle actual payment credentials - Only references/tokens to external gateway profiles are used - Payment data remains in external gateway's PCI-compliant vault - Shopify never receives sensitive card data **Best Practices:** 1. **Test First**: Test with sandbox/test gateway credentials before production 2. **Verify Gateway Active**: Ensure external gateway is properly configured in Shopify 3. **Use Customer ID**: Prefer `customerId` over `email` to avoid ambiguity 4. **Handle Errors**: Implement robust error handling for gateway failures 5. **Validate Externally**: Verify payment profiles exist in external gateway before associating 6. **Document IDs**: Keep mapping of Shopify customer IDs to external gateway IDs **Migration Workflow Example:** ``` For each legacy subscription: 1. Get customer email and Stripe customer ID from legacy system 2. Call this endpoint to link Shopify customer to Stripe customer 3. Create subscription contract in Shopify using returned payment instrument 4. Verify subscription created successfully 5. Mark legacy subscription as migrated 6. Test billing with external gateway ``` **Troubleshooting:** **"More than one customer found for email"**: - Multiple Shopify customers exist with same email - Solution: Use specific `customerId` instead of email **"Invalid payment gateway"**: - Gateway name misspelled or unsupported - Solution: Use exact values: `stripe`, `braintree`, `paypal`, `authorize_net` **"customerProfileId required"**: - Missing required parameter for Stripe/Braintree/Authorize.Net - Solution: Provide customer ID from external gateway **Response Format:** Returns Shopify's raw GraphQL mutation response containing payment instrument details and success/error information. **Related Endpoints:** - `GET /api/external/v2/subscription-contract-details/shopify/customer/{customerId}/payment-methods` - Verify association successful - `PUT /api/external/v2/subscription-contracts-update-payment-method` - Use associated payment for subscription **Authentication:** Requires valid X-API-Key header # Cancel a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/cancel-a-subscription-contract /subscription/admin-api-swagger.json delete /api/external/v2/subscription-contracts/{id} Cancels an existing subscription contract. This operation terminates all future billing and delivery cycles for the subscription. The cancellation takes effect immediately. **Important Notes:** - Any pending/scheduled orders will be cancelled - The customer will receive a cancellation confirmation email if SUBSCRIPTION_CANCELLED email template is enabled - Cancellation feedback and notes are stored for analytics and reporting - Minimum cycle requirements ARE enforced via API (unlike internal cancellations) - If the subscription is under a free trial period, minimum cycles are not enforced **Validation Rules:** - Contract must exist and belong to the authenticated shop - Contract must not already be cancelled - Must have completed minimum required billing cycles (unless in trial) **Post-Cancellation:** - Status changes to 'cancelled' (lowercase) - Any pause settings are cleared - Cancellation feedback and notes are saved - Activity log entry is created - Cancellation email is sent (if enabled) - Any invalid discount codes are automatically removed **Authentication:** Requires valid X-API-Key header # Generate customer portal authentication token Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/generate-customer-portal-authentication-token /subscription/admin-api-swagger.json get /api/external/v2/customer-portal-token Generates a secure, time-limited authentication token that grants access to the customer portal. Supports lookup by either Shopify customer ID or customer email address, making it flexible for various integration patterns. **What This Endpoint Returns:** An encrypted JWT-like token that authenticates a customer for the subscription management portal, along with token metadata. Unlike the manage-subscription-link endpoint which returns a complete URL, this returns only the token itself. **Response Components:** **token (string):** - Encrypted authentication token - JWT-style format with cryptographic signature - Contains customer ID and shop information - Valid for 2 hours from generation - Cannot be forged or tampered with **customerId (long):** - Shopify customer ID (numeric) - Useful for verification - Same ID used to generate token **Customer Lookup Methods:** **Option 1: By Customer ID (Recommended)** ``` GET /api/external/v2/customer-portal-token?customerId=12345 ``` - Direct lookup by numeric Shopify customer ID - Can use GraphQL GID format (automatically parsed) - Fastest and most reliable - No ambiguity **Option 2: By Email Address** ``` GET /api/external/v2/customer-portal-token?email=customer@example.com ``` - Searches subscription database for matching email - Finds associated customer ID automatically - Useful when customer ID unknown - Fails if email not found or invalid **Parameter Validation:** - Exactly ONE of `customerId` or `email` must be provided - Providing neither: Returns 400 error - Providing both: customerId takes precedence - Email must exist in subscription database **Token Security:** **Encryption:** - Uses HMAC-SHA256 cryptographic signing - Secret key stored securely on server - Token includes tamper detection - Modification invalidates token **Expiration:** - Tokens expire exactly 2 hours after generation - Timestamp embedded in token payload - Verified on each use - Cannot be extended **Scope:** - Token tied to specific customer - Token tied to specific shop - Cannot be used for other customers - Cannot be used across shops **Use Cases:** **1. Custom Portal Implementations:** - Build custom authentication flows - Integrate portal into existing apps - Create native mobile app authentication - Headless commerce integrations **2. API-First Architectures:** - Generate tokens programmatically - Pass tokens to frontend applications - Build microservice authentication - Separate auth from presentation **3. Single Sign-On (SSO):** - Authenticate users from existing system - Bypass password entry - Seamless portal access - Cross-platform authentication **4. Email/SMS Campaigns:** - Generate tokens for magic links - Embed in notification emails - Include in SMS messages - Create passwordless login links **5. Customer Support Tools:** - Generate portal access for support agents - View customer's portal perspective - Troubleshoot portal issues - Assist customers remotely **Response Format:** ```json { "customerId": 12345, "token": "eyJhbGciOiJIUzI1NiJ9.eyJjdXN0b21lcklkIjoxMjM0NSwic2hvcCI6Im15c3RvcmUubXlzaG9waWZ5LmNvbSIsInRpbWVzdGFtcCI6MTcwOTU2MjAwMH0.abc123xyz789" } ``` **Using the Token:** **Append to Portal URL:** ```javascript const { token } = await getCustomerPortalToken(customerId); const portalUrl = `https://mystore.com/tools/recurring/customer_portal?token=${token}`; window.location.href = portalUrl; ``` **Store in Session:** ```javascript // Store for authenticated API calls sessionStorage.setItem('portalToken', response.token); sessionStorage.setItem('customerId', response.customerId); // Use in subsequent requests fetch('/api/subscription-data', { headers: { 'Authorization': `Bearer ${sessionStorage.getItem('portalToken')}` } }); ``` **Mobile App Authentication:** ```javascript // Generate token server-side const tokenData = await generateToken(email); // Send to mobile app return { authToken: tokenData.token, customerId: tokenData.customerId, expiresIn: 7200 // 2 hours in seconds }; ``` **Important Considerations:** **Token vs. Full URL:** - This endpoint: Returns token only - `/manage-subscription-link` endpoint: Returns complete URL - Use this for custom implementations - Use manage-subscription-link for simple email links **Email Lookup Limitations:** - Email must exist in subscription database - Searches only customers with subscriptions - Won't find customers without subscriptions - Case-sensitive in some databases **Customer ID Formats:** - Accepts numeric ID: `12345` - Accepts GraphQL GID: `gid://shopify/Customer/12345` - Automatically extracts numeric portion - Always stores numeric format **Best Practices:** 1. **Generate On-Demand**: Create tokens when needed, not in advance 2. **Don't Store Long-Term**: Tokens expire in 2 hours 3. **Use HTTPS**: Always transmit tokens over secure connections 4. **Validate Expiry**: Check token age on frontend 5. **Prefer Customer ID**: Use customerId lookup when available 6. **Handle Errors**: Gracefully handle missing customers **Security Notes:** - Treat tokens like passwords - Don't log tokens in plain text - Don't expose in URLs if possible (use POST bodies) - Rotate tokens frequently - Monitor for suspicious token generation patterns **Comparison with Other Endpoints:** **vs. /manage-subscription-link:** - This: Token only - That: Complete URL - Use this for APIs, that for emails **vs. /subscription-contracts-email-magic-link:** - This: Returns token - That: Sends email - Use this for programmatic access, that for customer notifications **Authentication:** Requires valid X-API-Key header # Remove discount from subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/remove-discount-from-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-remove-discount Removes a specific discount from a subscription contract based on the discount ID. This will affect the pricing of all future orders generated by the subscription. **Key Features:** - Removes any type of discount: automatic, manual, or code-based - Uses Shopify's draft system for safe removal - Immediate effect on future order pricing - Cannot be undone - discount must be re-applied if needed - Activity log tracks removed discounts **Finding Discount IDs:** Discount IDs can be found in the subscription contract's discount collection: - Query the subscription contract to see `discounts.edges[].node.id` - Format: `gid://shopify/SubscriptionManualDiscount/123456` - Each discount has a unique ID regardless of type **Types of Discounts:** - **Manual Discounts**: Applied via API (percentage, fixed, price) - **Code Discounts**: Applied using discount codes - **Automatic Discounts**: Applied by Shopify based on rules - **Build-a-Box Discounts**: Volume-based discounts - **Selling Plan Discounts**: Built into the subscription plan **Impact on Pricing:** When a discount is removed: - Future orders use full price (no discount) - Existing orders are not affected - Other discounts remain active - Shipping prices may be recalculated - Customer sees updated pricing immediately **Common Use Cases:** - End promotional period discounts - Remove expired or invalid discount codes - Clear discounts before applying new ones - Customer service adjustments - Correct pricing errors **Important Notes:** - Cannot remove selling plan built-in discounts - No customer notification sent (consider sending separately) - Cannot selectively remove - removes entire discount - Build-a-Box discounts may auto-reapply based on quantity - Some discounts may be required by business rules **Post-Removal Considerations:** - Customer may notice price increase on next order - May affect customer retention if unexpected - Consider grace periods or notifications - Document reason for removal in your system **Authentication:** Requires valid X-API-Key header # Replace all line item attributes in bulk across subscription contracts Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/replace-all-line-item-attributes-in-bulk-across-subscription-contracts /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-bulk-update-line-item-attributes This API replaces all existing attributes for the specified line items within subscription contracts. Use carefully, as any attributes not included in the request will be removed. # Retrieve raw Shopify GraphQL response for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/retrieve-raw-shopify-graphql-response-for-a-subscription-contract /subscription/admin-api-swagger.json get /api/external/v2/contract-raw-response Fetches the complete, unprocessed JSON response directly from Shopify's GraphQL API for the specified subscription contract. This endpoint returns the full subscription data structure as provided by Shopify, including all nested objects and relationships. **Use Cases:** - Debugging subscription issues - Accessing all available subscription data - Understanding the complete data structure - Building custom integrations **Response Structure:** The response includes complete details about: - Customer information (email, name, ID) - Line items (products, quantities, pricing) - Billing and delivery policies - Payment method details - Discounts and pricing policies - Order history and billing attempts - Custom attributes and notes **Note:** This is a direct Shopify response with GraphQL type information (__typename fields) **Authentication:** Requires valid X-API-Key header # Retrieve subscription contracts Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/retrieve-subscription-contracts /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details Retrieves a paginated list of subscription contracts with powerful filtering capabilities. This endpoint enables complex queries to find specific subscriptions based on various criteria including dates, customer information, product details, and subscription characteristics. **Key Features:** - Comprehensive filtering across 20+ parameters - Full pagination support with customizable page size - Partial text matching for customer and order searches - Date range filtering for multiple date fields - Product and variant filtering within line items - Order amount range filtering - Plan type differentiation (prepaid vs pay-per-delivery) **Pagination:** - Uses Spring's Pageable format - Default page size: 20 items - Maximum page size: 2000 items (exceeding this limit will result in a 400 error) - Returns total count in X-Total-Count header - Provides navigation links in Link header **IMPORTANT - Sorting Limitations:** Due to the use of native SQL queries, sort parameters MUST use database column names (snake_case) rather than Java field names (camelCase). Common sorting examples: - `sort=next_billing_date,asc` (NOT nextBillingDate) - `sort=customer_name,desc` (NOT customerName) - `sort=created_at,asc` (NOT createdAt) - `sort=subscription_contract_id,desc` (NOT subscriptionContractId) - `sort=order_amount,asc` (NOT orderAmount) **Date Format:** All date parameters use ISO 8601 format with timezone: `yyyy-MM-dd'T'HH:mm:ssXXX` Examples: `2024-03-15T10:30:00+00:00` or `2024-03-15T10:30:00Z` **Date Range Filtering:** - For created, updated dates: Can use either 'from', 'to', or both - For next billing date: Both 'from' and 'to' must be provided together (current limitation) **Text Search Behavior:** - Customer name searches both name and email fields - Order name searches current and historical orders (in subscription_billing_attempt table) - All text searches are case-insensitive - Partial matches are supported (contains logic) - Subscription contract ID search supports partial numeric matching **nextPaidBillingDate:** - The next billing date on which the customer is actually charged. - For prepaid/cycle-based plans with free or discounted reward cycles, this skips any upcoming cycle that is fully free and lands on the next cycle that gets charged - it can differ from `nextBillingDate` in that case. - For plans without free cycles, or when no billing attempt has been scheduled yet, this equals `nextBillingDate`. - Returns `null` only when no future paid cycle exists (e.g., all remaining cycles are free and the plan's max cycle count has been reached). **Plan Type Classification:** - **prepaid**: Billing interval > delivery interval - **non-prepaid**: Billing interval = delivery interval **Performance Considerations:** - Complex filters (especially JSON searches and order amount calculations) may impact response time - Use specific filters when possible - Consider smaller page sizes for complex queries - Product, variant, and selling plan searches use JSON_SEARCH which may be slow on large datasets **Authentication:** Requires valid X-API-Key header (the deprecated 'api_key' query parameter is still accepted but not recommended) # Send magic link email to customer for portal access Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/send-magic-link-email-to-customer-for-portal-access /subscription/admin-api-swagger.json get /api/external/v2/subscription-contracts-email-magic-link Sends an automated email to a customer containing a secure magic link for accessing their subscription management portal. The email is sent using the shop's configured email template and includes a time-limited authentication token. **What This Endpoint Does:** 1. Validates customer exists and has subscriptions 2. Generates secure portal access token 3. Retrieves shop's email template configuration 4. Sends personalized email with magic link 5. Logs activity for audit trail **Magic Link Functionality:** **What is a Magic Link?** A magic link is a special URL containing an encrypted authentication token that allows customers to access their portal without entering a password. Clicking the link automatically logs them in. **Link Contents:** - Shop's portal URL - Encrypted customer token (2-hour expiration) - Direct access to subscription management - No password required **Security:** - Token expires in 2 hours - Single customer authentication - Cannot be used by others - Logged for security audit **Request Parameters:** **email (required):** - Customer's email address - Must exactly match email in Shopify - Customer must have at least one subscription - Case-sensitive in some systems **Email Template Configuration:** **Template Requirements:** - Email template must be configured in Appstle settings - Template type: SUBSCRIPTION_MANAGEMENT_LINK - Template must not be disabled - Template includes shop branding and customization **Email Content:** - Personalized greeting with customer name - Clickable magic link button/link - Expiration notice (link valid 2 hours) - Shop branding and footer - Optional custom messaging **Template Variables:** - `{customer_name}`: Customer's display name - `{magic_link}`: Portal URL with token - `{shop_name}`: Store name - `{expiration_time}`: Token expiry time **Use Cases:** **1. Customer Self-Service:** - "Forgot password" alternative - Quick portal access without account setup - Passwordless authentication flow - Reduce friction for customers **2. Subscription Management Prompts:** - "Manage your subscription" emails - Pre-billing reminders with management link - Post-purchase subscription setup - Re-engagement campaigns **3. Customer Support:** - Send portal access to customers - Enable self-service during support interactions - Provide instant portal access - Reduce support workload **4. Automated Workflows:** - Payment failure recovery emails - Subscription expiration notices - Pause/skip reminders - Renewal notifications **5. Marketing Campaigns:** - Subscription feature announcements - New product availability - Loyalty program invitations - Referral program links **Process Flow:** ``` 1. API receives email parameter 2. Searches subscription database for customer email 3. If not found → Returns 400 error 4. If found → Retrieves customer subscriptions 5. Sorts subscriptions by status (active first) 6. Checks email template configuration 7. If disabled → Returns 400 error with instructions 8. If enabled → Generates magic link token 9. Prepares email with template 10. Sends email via configured provider 11. Logs activity (source: MERCHANT_EXTERNAL_API) 12. Returns success message ``` **Response Format:** ```json "Email triggered successfully." ``` **Simple string response** confirming email queued for delivery. **Important Considerations:** **Customer Validation:** - Email MUST exist in subscription database - Customer MUST have at least one subscription - Customers without subscriptions cannot receive link - Returns error if customer not found **Email Template Disabled:** - If template disabled in settings: Returns error - Error message guides merchant to enable template - Path: "More -> Notification Settings" - Template must be explicitly enabled **Email Delivery:** - Email sent asynchronously - Success response doesn't guarantee delivery - Check email logs for delivery confirmation - Respects shop's email provider settings **Multiple Subscriptions:** - If customer has multiple subscriptions: Sorts by status - Active subscriptions shown first - Link provides access to ALL customer subscriptions - Portal displays all contracts **Activity Logging:** - All magic link emails logged - Source: MERCHANT_EXTERNAL_API - Includes timestamp and customer - Viewable in activity logs **Integration Examples:** **Webhook Trigger - Payment Failed:** ```javascript async function handlePaymentFailure(webhook) { const customerEmail = webhook.customer.email; // Send magic link to customer await fetch( `/api/external/v2/subscription-contracts-email-magic-link?email=${customerEmail}`, { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY } } ); console.log(`Magic link sent to ${customerEmail} for payment update`); } ``` **Customer Support Button:** ```javascript async function sendPortalAccess(customerEmail) { try { const response = await fetch( `/api/external/v2/subscription-contracts-email-magic-link?email=${encodeURIComponent(customerEmail)}`, { headers: { 'X-API-Key': apiKey }, method: 'GET' } ); if (response.ok) { alert('Portal access email sent to customer!'); } } catch (error) { console.error('Failed to send magic link:', error); } } ``` **Best Practices:** 1. **Validate Email**: Check email format before calling API 2. **Rate Limiting**: Don't spam customers - limit frequency 3. **Error Handling**: Handle customer not found gracefully 4. **User Feedback**: Confirm email sent to user 5. **Test Template**: Ensure email template configured and working 6. **Monitor Logs**: Check activity logs for delivery issues **Common Errors:** **"Customer Email does not exist":** - Email not found in subscription database - Customer has no subscriptions - Email may be misspelled **"Email template not found":** - SUBSCRIPTION_MANAGEMENT_LINK template not configured - Contact Appstle support to set up template **"Email is currently disabled":** - Template disabled in Notification Settings - Navigate to More -> Notification Settings - Enable "Subscription Management Link" email **Authentication:** Requires valid X-API-Key header # Update billing interval for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-billing-interval-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-billing-interval Updates the billing frequency (how often the customer is charged) for a subscription contract. This comprehensive operation recalculates billing dates, adjusts pricing, updates selling plans, and may also modify delivery intervals. **Key Features:** - Changes billing frequency while maintaining subscription continuity - Automatically adjusts delivery interval when linked to billing - Recalculates next billing date based on store settings - Reprices all line items for the new frequency - Finds and applies matching selling plans - Handles anchor day adjustments for consistent billing - Validates prepaid subscription constraints **Billing Interval Types:** - **DAY**: Daily billing (use with caution) - **WEEK**: Weekly billing (e.g., every 1, 2, 3 weeks) - **MONTH**: Monthly billing (e.g., every 1, 2, 3 months) - **YEAR**: Annual billing **Validation Rules:** - Cannot set the same interval as current (no-op prevention) - For prepaid subscriptions: billing interval must exceed delivery interval - Example: Can't bill monthly if delivering weekly (would be paying for 1 delivery but receiving 4) **Next Billing Date Calculation:** The system uses sophisticated logic to determine the new billing date: 1. Starts from current or last successful billing date 2. Applies store timezone and order time preferences 3. Ensures date is in the future 4. Respects day-of-week preferences if configured 5. May keep current date if 'enableChangeFromNextBillingDate' is false **Side Effects:** - **Delivery Interval**: Updated if currently equal to billing interval - **Line Item Pricing**: Recalculated based on new frequency multiplier - **Selling Plans**: Finds and applies best matching plans for products - **Anchor Days**: Updates billing anchor for consistent scheduling - **Email Notifications**: Sends 'ORDER_FREQUENCY_UPDATED' to customer - **Activity Logs**: Records both billing and delivery changes **Prepaid vs Pay-Per-Delivery:** - Pay-per-delivery: Billing and delivery intervals typically match - Prepaid: Customer pays upfront for multiple deliveries - This endpoint enforces prepaid logic to prevent undercharging **Authentication:** Requires valid X-API-Key header # Update line item attributes for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-line-item-attributes-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-attributes Updates the custom attributes for a specific line item within a subscription contract. This endpoint accepts a contract ID, a line item ID, and a list of attribute objects (each containing key/value pairs) to update. It uses the underlying service to perform validation (such as contract existence, freeze status, and attribute-based cycle limits) before applying the update. The operation is intended for external API consumers, and authentication is performed via the X-API-Key header. Deprecated api_key parameter is supported for backward compatibility. # Update multiple line item attributes for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-multiple-line-item-attributes-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-multiple-line-item-attributes Updates custom attributes for multiple line items within a subscription contract. The request body must be a JSON object where each key is a line item identifier (as a String) and each value is a list of AttributeInfo objects containing attribute key/value pairs. For example: { "gid://shopify/SubscriptionLineItem/987654321": [ { "key": "color", "value": "red" }, { "key": "size", "value": "M" } ] }. The service validates that the contract exists and that each specified line item is eligible for update. Authentication is performed via the X-API-Key header (the 'api_key' parameter is deprecated). # Update next billing date for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-next-billing-date-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-billing-date Reschedules the next billing date for an active subscription contract. This endpoint allows you to change when the next order will be created and processed. **Key Features:** - Updates the next billing date to the specified date - Optionally reschedules all future orders based on the new date - Syncs the updated date to Shopify - Sends confirmation email to customer **The rescheduleFutureOrder Parameter:** - **true** (default): Updates the next billing date AND recalculates all future queued orders based on the new date. Use this to shift the entire billing schedule. - **false**: Only updates the next billing date. Other queued orders remain unchanged. Use this for one-time date adjustments. **Important Notes:** - The new date must be in the future (with 10 minute grace period) - Date is validated against the shop's timezone - If anchor days are configured, future orders may align to those anchors **Process Flow:** 1. Validates the contract exists and belongs to the shop 2. Validates the new date is not in the past 3. Updates the billing attempt to the new date 4. Syncs the updated nextBillingDate to Shopify 5. If rescheduleFutureOrder=true, regenerates the queue from the new date 6. Sends confirmation email to customer 7. Records activity log entry **Date Format:** - Must be ISO 8601 format with timezone - Examples: `2024-03-15T12:00:00Z`, `2024-03-15T12:00:00+05:30` - URL encode the date when passing as query parameter **Timezone Handling:** - The provided date is used as-is for the billing attempt - Validation (past date check) uses the shop's configured timezone - All dates in the response are in UTC (Z suffix) **Date Restrictions (Customer Portal Only):** When called from customer portal context: - Minimum days from today (skipDaysFromCurrentDate setting) - Maximum days from today (billingDateRestrictToDays setting) - External API calls bypass these restrictions **Authentication:** Requires valid X-API-Key header # Update Subscription contract frequency by compatible selling-plan frequency Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-subscription-contract-frequency-by-compatible-selling-plan-frequency /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-frequency-by-compatible-plan Strict compatible frequency update. Use frequencyKey from /api/data/external/v2/compatible-selling-plan-frequencies or a sellingPlanId that belongs to one compatible frequency option. The existing /api/external/v2/subscription-contracts-update-frequency-by-selling-plan endpoint remains legacy for backward compatibility. # Update Subscription contract frequency by selling plan Source: https://developers.appstle.com/subscription-admin-api/subscription-contracts/update-subscription-contract-frequency-by-selling-plan /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-frequency-by-selling-plan # Get delivery options for subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-data/get-delivery-options-for-subscription-contract /subscription/admin-api-swagger.json get /api/external/v2/data/contract-delivery-options Retrieves all valid delivery methods available for a specific subscription contract. Returns shipping profiles and delivery options based on the contract's delivery address and product characteristics. **Delivery Information Returned:** - Available shipping profiles - Delivery methods for each profile - Shipping rates and costs - Delivery speed (standard, express, etc.) - Method names and descriptions - Eligibility based on address and products **Filtering Behavior:** By default, returns only delivery methods valid for the contract: - Matches delivery address country/region - Compatible with subscription products - Available for contract weight/dimensions **Include All Methods:** Use header `X-Include-All-Methods: true` to return all delivery methods regardless of eligibility. Useful for admin UIs where you want to show all options. **Use Cases:** - Display delivery method selector in customer portal - Allow customers to change shipping method - Calculate shipping costs for subscription - Validate delivery method during subscription updates - Show upgrade options (standard to express) **Common Scenarios:** **Customer Portal - Change Delivery Speed:** 1. Call this endpoint with contractId 2. Display available methods to customer 3. Customer selects preferred method 4. Update subscription with new delivery method ID **Subscription Creation - Delivery Selection:** 1. Get delivery options for draft contract 2. Present options during checkout/signup 3. Create subscription with selected method **Important Notes:** - Delivery options depend on current contract address - Changing address may change available methods - Costs may vary based on products in subscription - Some methods may have minimum order requirements - International shipping may have additional restrictions **Authentication:** Requires X-API-Key header # Create a new subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/create-a-new-subscription-contract /subscription/admin-api-swagger.json post /api/external/v2/subscription-contract-details/create-subscription-contract Creates a new subscription contract for a customer with specified products, billing frequency, and delivery details. This endpoint allows you to programmatically create subscriptions with custom pricing, delivery schedules, and multiple line items. The subscription will be created in Shopify and synchronized with the Appstle system. **Important Notes:** - Customer must have at least one valid payment method - If paymentMethodId is not provided, the default payment method will be used - If delivery interval is not specified, it defaults to the billing interval - Custom pricing policies can be applied per line item - All monetary values are in the store's base currency unless currencyCode is specified - CustomAttributes and line item customAttributes use Shopify's AttributeInput format: {"key": "string", "value": "string"} **Pricing Policy Types:** - SELLING_PLAN_PRICING_POLICY: Uses the default pricing from the selling plan - CUSTOM_PRICING_POLICY: Allows custom discount cycles defined in pricingPolicy array - NO_PRICING_POLICY: Uses the base price without any discounts **Authentication:** Requires valid X-API-Key header # Get available billing intervals for selling plan(s) Source: https://developers.appstle.com/subscription-admin-api/subscription-management/get-available-billing-intervals-for-selling-plans /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/billing-interval Retrieves all available billing frequency options configured for specific Shopify selling plan(s). Returns the complete set of billing intervals that customers can choose from, including frequency, interval type, and any associated discounts or pricing policies. **What This Endpoint Does:** Queries the subscription group plans database to find all frequency configurations associated with given selling plan ID(s). This is essential for building subscription frequency selectors in customer portals or during subscription modifications. **Key Concepts:** **Selling Plans:** - Shopify's mechanism for defining subscription options - Each product variant can have multiple selling plans - Selling plans define billing/delivery frequency - Plans are grouped in subscription groups **Frequency Info:** - Specific billing interval configuration - E.g., "Every 2 weeks", "Every month", "Every 3 months" - Includes pricing policies and discounts - Customers select from available frequencies **Request Parameters:** **sellingPlanIds (required):** - Comma-separated list of Shopify selling plan IDs - Example: `"123456,123457,123458"` - Can query single or multiple plans - Returns frequencies for ALL provided plans **Lookup Process:** 1. Parse selling plan IDs from comma-separated string 2. Find subscription groups containing these plans 3. Extract frequency configurations from group JSON 4. Return all matching frequency options 5. Deduplicate if same frequency appears multiple times **Response Data Included:** **For Each Frequency Option:** - **id**: Selling plan ID - **frequencyName**: Display name (e.g., "Monthly") - **interval**: WEEK, MONTH, or YEAR - **intervalCount**: Number of intervals (e.g., 2 for bi-weekly) - **deliveryInterval**: Same or different from billing - **deliveryIntervalCount**: Delivery frequency - **pricingPolicy**: Discount configuration - **billingPolicy**: Min/max cycles, anchor settings **Pricing Policy Details:** - Discount type (percentage, fixed amount) - Discount value - After cycle discounts (e.g., "50% off first 3 months") - Adjustment type **Use Cases:** **1. Frequency Selection UI:** - Build dropdown/radio list of frequency options - Show available intervals to customers - Display pricing for each frequency - Enable subscription frequency changes **2. Subscription Modification:** - Show current frequency and alternatives - Allow customers to switch frequencies - Validate new frequency selection - Preview pricing changes **3. Product Page:** - Display subscription frequency options - Show "Subscribe and save" pricing - Calculate savings per frequency - Build subscription purchase selectors **4. Customer Portal:** - "Change Frequency" functionality - Show all available options - Highlight current selection - Display pricing differences **Response Format:** ```json [ { "id": "123456", "frequencyName": "Every 2 Weeks", "interval": "WEEK", "intervalCount": 2, "deliveryInterval": "WEEK", "deliveryIntervalCount": 2, "pricingPolicy": { "adjustmentType": "PERCENTAGE", "adjustmentValue": "10.0" } }, { "id": "123457", "frequencyName": "Monthly", "interval": "MONTH", "intervalCount": 1, "deliveryInterval": "MONTH", "deliveryIntervalCount": 1, "pricingPolicy": { "adjustmentType": "PERCENTAGE", "adjustmentValue": "15.0" } } ] ``` **Integration Example:** **Customer Portal - Frequency Selector:** ```javascript // Get selling plan from current subscription const currentSellingPlanId = subscription.sellingPlanId; // Fetch available frequencies const frequencies = await fetch( `/api/external/v2/subscription-contract-details/billing-interval?sellingPlanIds=${currentSellingPlanId}`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); // Build selector const selector = frequencies.map(freq => ` `).join(''); document.querySelector('#frequency-select').innerHTML = selector; ``` **Important Considerations:** **Data Source:** - Queries Appstle database (NOT Shopify API) - Based on subscription group configuration - Fast response (< 100ms typically) - Data synced when groups are updated **Multiple Selling Plans:** - Can query multiple plans at once - Returns union of all frequencies - Useful for products with multiple subscription options - Results may contain duplicates if plans share frequencies **Empty Results:** - Returns empty array `[]` if no plans found - Returns empty if selling plan ID invalid - Not an error - handle gracefully **Best Practices:** 1. **Query Relevant Plans**: Only query selling plans for current product/variant 2. **Display Discounts**: Show savings clearly in UI 3. **Sort by Interval**: Order options logically (weekly → monthly → yearly) 4. **Highlight Current**: Clearly mark customer's current frequency 5. **Cache Results**: Cache frequency data per selling plan **Authentication:** Requires valid X-API-Key header # Get current billing cycle number for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/get-current-billing-cycle-number-for-a-subscription-contract /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/current-cycle/{contractId} Retrieves the current billing cycle number for a specific subscription contract. The cycle number represents how many successful billing attempts have occurred for this subscription, starting from 1 for the initial order. **What is a Billing Cycle?** A billing cycle represents one completed billing period in a subscription's lifetime. Each successful billing attempt increments the cycle count. This number is crucial for: - Tracking subscription progress towards minimum/maximum cycle limits - Applying cycle-based pricing adjustments (discounts after N cycles) - Determining eligibility for cancellation (minimum cycles requirement) - Calculating customer lifetime value - Analyzing subscription retention metrics **How Cycle Counting Works:** **Initial Order:** - Cycle 1 starts when subscription is first created - Initial order counts as the first billing cycle - Includes origin order that created the subscription **Subsequent Orders:** - Each successful billing attempt increments cycle by 1 - Only SUCCESS status billing attempts are counted - Failed/skipped billing attempts do NOT increment cycle - Paused subscriptions maintain their current cycle number **Calculation Formula:** ``` Current Cycle = 1 + (Number of Successful Billing Attempts) ``` **Example Timeline:** - Day 1: Subscription created, initial order → Cycle 1 - Day 30: First recurring order successful → Cycle 2 - Day 60: Second recurring order successful → Cycle 3 - Day 90: Billing fails (payment declined) → Still Cycle 3 - Day 95: Retry successful → Cycle 4 **Use Cases:** **1. Cancellation Eligibility:** - Verify customer has met minimum cycle requirement - Enforce contract terms (e.g., "3 month minimum") - Display "Can cancel after N more orders" messaging - Block premature cancellations **2. Pricing Adjustments:** - Apply introductory pricing for first N cycles - Trigger loyalty discounts after X cycles - Calculate when pricing changes take effect - Implement "First 3 months 50% off" promotions **3. Customer Retention:** - Identify subscriptions at risky cycle counts - Send retention campaigns at specific milestones - Track average cycles before churn - Celebrate subscription anniversaries **4. Analytics & Reporting:** - Calculate customer lifetime value (cycle × price) - Analyze subscription duration distribution - Track retention curves by cohort - Measure success of lifecycle campaigns **5. Customer Portal Display:** - Show "Order #X of Y" progress indicators - Display remaining cycles until cancellation allowed - Show subscription tenure/loyalty status - Calculate and display subscription value earned **Response Format:** Returns a single integer representing the current cycle number: ```json 3 ``` **Response Examples:** **New subscription (just created):** ```json 1 ``` **After 5 successful billing attempts:** ```json 6 ``` Note: Initial order (1) + 5 successful renewals = Cycle 6 **Important Considerations:** **Cycle vs. Billing Attempts:** - Failed billing attempts don't increment cycle - Skipped orders don't increment cycle - Manual order creation may not increment cycle - Cycle represents successful billing events only **Paused Subscriptions:** - Cycle number remains frozen while paused - Resumes at same cycle when un-paused - Pause duration doesn't affect cycle count **Minimum/Maximum Cycles:** - `minCycles`: Minimum cycles before cancellation allowed - `maxCycles`: Subscription auto-expires after this many cycles - Use this endpoint to check progress towards these limits **Data Source:** - Queries Appstle database (not Shopify API) - Counts records in subscription_billing_attempt table - Filters by SUCCESS status only - Fast response time (< 100ms typically) **Integration Example:** **Check if customer can cancel:** ```javascript // Get subscription details const contract = await getSubscriptionContract(contractId); const currentCycle = await fetch( `/api/external/v2/subscription-contract-details/current-cycle/${contractId}`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); const minCycles = contract.minCycles || 0; if (currentCycle >= minCycles) { console.log('Customer can cancel now'); showCancelButton(); } else { const cyclesRemaining = minCycles - currentCycle; console.log(`Must complete ${cyclesRemaining} more orders before canceling`); showMinimumCommitmentMessage(cyclesRemaining); } ``` **Display progress to max cycles:** ```javascript const currentCycle = await getCycleNumber(contractId); const maxCycles = contract.maxCycles; if (maxCycles) { const progress = (currentCycle / maxCycles) * 100; console.log(`Subscription ${progress.toFixed(0)}% complete (${currentCycle}/${maxCycles})`); if (currentCycle === maxCycles) { console.log('This is the final cycle - subscription will expire after this order'); } } ``` **Performance Characteristics:** **Fast Query:** - Simple database count query - Indexed by shop and contractId - Typical response time: 50-150ms - Suitable for real-time UI updates **Best Practices:** 1. **Cache Results**: Cache cycle number briefly (few minutes) to reduce API calls 2. **Combine with Contract Data**: Fetch contract details simultaneously for min/max cycles 3. **Handle Edge Cases**: Account for subscriptions with no successful billings yet 4. **Display Progress**: Show cycle number in customer-friendly format ("Order 3 of 12") 5. **Sync with Billing**: Update cycle number after each billing attempt completes **Common Misunderstandings:** **Myth: Cycle = Months Subscribed** - Reality: Cycle = Successful billing attempts, not time elapsed - A paused subscription stays at same cycle for months - Failed payments don't advance the cycle **Myth: First order is Cycle 0** - Reality: Cycles start at 1, not 0 - Initial/origin order is Cycle 1 **Related Fields:** - `minCycles`: Minimum cycles before cancellation (from contract) - `maxCycles`: Maximum cycles before auto-expiry (from contract) - `billingInterval`: Frequency between cycles (from contract) **Authentication:** Requires valid X-API-Key header # Get raw Shopify GraphQL contract response Source: https://developers.appstle.com/subscription-admin-api/subscription-management/get-raw-shopify-graphql-contract-response /subscription/admin-api-swagger.json get /api/external/v2/subscription-contracts/contract-external/{contractId} Retrieves the complete, unprocessed subscription contract data directly from Shopify's GraphQL API. This endpoint returns the full Shopify subscription contract object exactly as Shopify provides it, including all nested fields, relationships, and GraphQL metadata. **What This Endpoint Returns:** Unlike processed/transformed endpoints, this returns Shopify's raw SubscriptionContract GraphQL object with all available fields. This is the same data structure you would receive if querying Shopify's GraphQL API directly, making it ideal for: - Debugging Shopify data synchronization issues - Accessing fields not exposed in other endpoints - Understanding complete Shopify contract structure - Building custom integrations requiring full data - Comparing Shopify source data with processed data **Data Included:** **Contract Core:** - Full subscription contract object from Shopify - All GraphQL __typename fields preserved - Complete nested object structures - All available Shopify contract fields **Customer Information:** - Complete customer object with all fields - Default address details - Customer tags and metadata - Marketing preferences **Line Items:** - Full line item details with edges/nodes structure - Product and variant complete objects - Pricing policies with all cycle discounts - Line item custom attributes - Current and original prices **Billing & Delivery:** - Complete billing policy object - Full delivery policy details - Delivery method with all fields - Delivery price breakdown - Billing anchor details **Payment Information:** - Payment method details (if available) - Customer payment instrument - Payment gateway information **Orders & History:** - Origin order details - Last payment status - Historical billing attempts (in Appstle) **Use Cases:** **1. Debugging & Troubleshooting:** - Investigate data sync discrepancies - Verify what Shopify actually stores - Debug webhook payload issues - Compare expected vs actual data **2. Advanced Integrations:** - Access Shopify-specific fields not in standard APIs - Build custom analytics using raw data - Integrate with Shopify GraphQL directly - Extract fields for custom processing **3. Data Analysis:** - Analyze complete contract structure - Extract all available metadata - Build comprehensive data exports - Perform deep data audits **4. Development & Testing:** - Understand Shopify's data model - Test new feature development - Verify API responses - Documentation and examples **Response Structure:** Returns SubscriptionContractQuery.SubscriptionContract object: ```json { "id": "gid://shopify/SubscriptionContract/123456789", "status": "ACTIVE", "nextBillingDate": "2024-03-15T00:00:00Z", "customer": { "id": "gid://shopify/Customer/987654321", "email": "customer@example.com", "displayName": "John Doe", "__typename": "Customer" }, "lines": { "edges": [ { "node": { "id": "gid://shopify/SubscriptionLine/111111", "quantity": 2, "variantId": "gid://shopify/ProductVariant/222222", "title": "Monthly Coffee Box", "variantTitle": "Medium Roast", "currentPrice": { "amount": "29.99", "currencyCode": "USD" }, "pricingPolicy": {...}, "__typename": "SubscriptionLine" } } ] }, "billingPolicy": { "interval": "MONTH", "intervalCount": 1, "minCycles": 3, "maxCycles": 12 }, "deliveryPolicy": {...}, "deliveryMethod": {...}, "__typename": "SubscriptionContract" } ``` **Important Considerations:** **Data Source:** - Queries Shopify GraphQL API in real-time - NOT cached in Appstle database - Always returns current Shopify state - Subject to Shopify API rate limits **Performance:** - Slower than database queries (500-1500ms typical) - Makes real-time call to Shopify - Response size can be large (10-50 KB) - Use sparingly to avoid rate limits **GraphQL Structure:** - Includes __typename fields throughout - Uses GraphQL ID format (gid://shopify/...) - Nested edges/nodes structure for lists - All fields as defined in Shopify's schema **Best Practices:** 1. **Use for Debugging**: Perfect for troubleshooting data issues 2. **Avoid Polling**: Don't call repeatedly - use cached/processed endpoints instead 3. **Cache Response**: Cache the response if using for display 4. **Parse Carefully**: Handle GraphQL structure (edges/nodes) 5. **Monitor Rate Limits**: Each call counts against Shopify API limits **When to Use vs Other Endpoints:** **Use this endpoint when:** - Need complete Shopify contract data - Debugging synchronization issues - Accessing Shopify-specific fields - Building Shopify GraphQL integrations **Use /api/external/v2/subscription-contract-details when:** - Need processed, filtered contract data - Want faster response times - Querying multiple contracts - Building customer-facing UIs **Authentication:** Requires valid X-API-Key header # Get subscription contract analytics and revenue metrics Source: https://developers.appstle.com/subscription-admin-api/subscription-management/get-subscription-contract-analytics-and-revenue-metrics /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/analytics/{contractId} Retrieves comprehensive analytics for a specific subscription contract, including total revenue generated, number of successful orders, and formatted revenue display. This endpoint provides key performance metrics for understanding the financial impact of individual subscriptions. **What This Endpoint Returns:** Financial and operational analytics for a single subscription contract, calculated from all successful billing attempts throughout the subscription's lifetime. Unlike realtime contract queries, this provides historical aggregated data. **Metrics Included:** **Revenue Metrics:** - **totalOrderAmount**: Total revenue in decimal format (e.g., 299.95) - **totalOrderRevenue**: Formatted currency string (e.g., "$299.95") - Calculated from all SUCCESS status billing attempts - Includes taxes, shipping, and discounts - Does NOT include failed or pending payments **Order Count:** - **totalOrders**: Count of successful billing attempts - Represents number of orders generated by subscription - Does NOT include failed billing attempts - Starts at 0 for brand new subscriptions **Currency Formatting:** - Uses shop's configured money_format - Respects shop's currency code (USD, EUR, GBP, etc.) - Handles currency symbols and decimal places correctly - Falls back to USD if currency unknown **Calculation Logic:** **Data Source:** ```sql SELECT COUNT(*) as totalOrders, SUM(order_amount) as totalOrderAmount FROM subscription_billing_attempt WHERE shop = ? AND contract_id = ? AND status = 'SUCCESS' ``` **What Gets Counted:** - ✅ Initial subscription order - ✅ All successful recurring orders - ✅ Manual billing attempts that succeeded - ✅ Retry attempts that eventually succeeded - ❌ Failed payment attempts - ❌ Skipped billing cycles - ❌ Cancelled/voided orders - ❌ Pending/in-progress billing **Use Cases:** **1. Customer Lifetime Value:** - Calculate total revenue from specific customer - Measure subscription ROI - Identify high-value subscriptions - Track revenue growth over time **2. Subscription Performance:** - Analyze revenue per subscription - Compare subscriptions by total value - Identify most profitable subscription types - Calculate average order value **3. Customer Portal:** - Display "You've saved $XXX" messaging - Show "X orders delivered" stats - Celebrate subscription milestones - Build customer loyalty through transparency **4. Reporting & Analytics:** - Build subscription revenue dashboards - Generate customer value reports - Track subscription health metrics - Forecast future revenue **5. Business Intelligence:** - Segment customers by subscription value - Identify churn risk (low order count) - Calculate retention rates - Measure subscription program success **Response Format:** ```json { "totalOrders": 12, "totalOrderAmount": 599.88, "totalOrderRevenue": "$599.88" } ``` **Response Fields:** - `totalOrders` (integer): Count of successful billing attempts - `totalOrderAmount` (decimal): Numeric revenue total - `totalOrderRevenue` (string): Formatted currency display **Example Scenarios:** **Brand New Subscription:** ```json { "totalOrders": 0, "totalOrderAmount": 0.00, "totalOrderRevenue": "$0.00" } ``` **After First Successful Order:** ```json { "totalOrders": 1, "totalOrderAmount": 49.99, "totalOrderRevenue": "$49.99" } ``` **Established Subscription (EUR):** ```json { "totalOrders": 24, "totalOrderAmount": 1199.76, "totalOrderRevenue": "€1.199,76" } ``` **Integration Examples:** **Customer Portal - Loyalty Display:** ```javascript const analytics = await fetch( `/api/external/v2/subscription-contract-details/analytics/${contractId}`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); const loyaltyMessage = `

Thank you for your loyalty!

You've received ${analytics.totalOrders} orders

Total value: ${analytics.totalOrderRevenue}

`; ``` **Revenue Dashboard:** ```javascript // Fetch analytics for multiple subscriptions const contractIds = [123, 456, 789]; const analyticsPromises = contractIds.map(id => getContractAnalytics(id) ); const allAnalytics = await Promise.all(analyticsPromises); const totalRevenue = allAnalytics.reduce( (sum, a) => sum + a.totalOrderAmount, 0 ); console.log(`Total subscription revenue: $${totalRevenue.toFixed(2)}`); ``` **Important Considerations:** **Data Accuracy:** - Based on Appstle's billing attempt records - May differ slightly from Shopify order totals - Includes only what Appstle successfully billed - Updated after each billing attempt completes **Currency Handling:** - Formatting uses shop's money_format setting - Different shops may have different decimal separators - Currency symbol placement varies by locale - Always use totalOrderAmount for calculations **Performance:** - Fast database aggregation query - Typical response time: 100-300ms - Indexed by shop and contract ID - Suitable for real-time display **Best Practices:** 1. **Use for Display**: Use totalOrderRevenue for customer-facing display 2. **Calculate with Amount**: Use totalOrderAmount for mathematical operations 3. **Cache Strategically**: Cache for dashboards, fetch real-time for critical displays 4. **Handle Zero**: Gracefully handle new subscriptions with 0 orders 5. **Validate Contract**: Ensure contract exists before querying analytics **Limitations:** **What's NOT Included:** - Future projected revenue - Failed payment attempt counts - Refunds or chargebacks - Pending/processing orders - Orders from other subscriptions **Related Calculations:** **Average Order Value:** ```javascript const aov = analytics.totalOrderAmount / analytics.totalOrders; console.log(`Average order value: $${aov.toFixed(2)}`); ``` **Customer Lifetime Value (projected):** ```javascript const monthsActive = calculateMonthsActive(contract.createdAt); const monthlyRevenue = analytics.totalOrderAmount / monthsActive; const projectedLTV = monthlyRevenue * 12; // Annual LTV ``` **Authentication:** Requires valid X-API-Key header # Get subscription fulfillment details for latest order Source: https://developers.appstle.com/subscription-admin-api/subscription-management/get-subscription-fulfillment-details-for-latest-order /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-details/subscription-fulfillments/{contractId} Retrieves fulfillment information for the most recent successful order associated with a subscription contract. This includes tracking details, shipment status, delivery estimates, and fulfillment line items from Shopify. **What This Endpoint Returns:** Complete fulfillment data for the subscription's last successfully billed order, queried in real-time from Shopify's GraphQL API. Shows customers when and how their subscription items will be (or were) delivered. **Fulfillment Data Included:** **Shipment Tracking:** - Tracking numbers and URLs - Carrier information (USPS, FedEx, UPS, etc.) - Tracking company details - Tracking status updates **Delivery Status:** - Fulfillment status (fulfilled, in_transit, out_for_delivery, delivered, etc.) - Estimated delivery date - Actual delivery timestamp (if delivered) - Delivery method (shipping, pickup, local delivery) **Fulfillment Details:** - Fulfilled line items (which products shipped) - Quantities fulfilled - Fulfillment service (manual, automated, 3PL) - Fulfillment created/updated dates - Multiple fulfillments (if order shipped in parts) **Location Information:** - Fulfillment location/warehouse - Origin address details - Fulfillment service name **Order Determination Logic:** **Which Order is Retrieved:** 1. Finds most recent SUCCESS billing attempt for contract 2. If found: Uses that order's Shopify order ID 3. If none: Falls back to subscription's origin order ID 4. Queries Shopify for that order's fulfillments **Why This Matters:** - Shows CURRENT/LATEST fulfillment status - Not historical fulfillments from months ago - Reflects what customer is waiting for NOW - Updates after each new billing cycle **Use Cases:** **1. Customer Portal "Where's My Order":** - Display tracking information - Show estimated delivery date - Provide carrier tracking links - Update fulfillment status **2. Subscription Order Tracking:** - Track recurring order deliveries - Monitor fulfillment progress - Identify delayed shipments - Provide proactive delivery updates **3. Customer Support:** - Answer "where is my order" questions - Verify shipment details - Troubleshoot delivery issues - Provide accurate tracking info **4. Automated Notifications:** - Send tracking emails automatically - Notify customers of shipments - Alert on delivery completion - Trigger review request flows **5. Subscription Management:** - Show fulfillment in subscription history - Display delivery patterns - Track fulfillment reliability - Monitor shipping performance **Response Structure:** Returns Shopify Order object with fulfillments: ```json { "id": "gid://shopify/Order/123456789", "name": "#1001", "fulfillmentOrders": { "edges": [ { "node": { "id": "gid://shopify/FulfillmentOrder/111111", "status": "SUCCESS", "fulfillments": { "edges": [ { "node": { "trackingInfo": [ { "number": "1Z999AA10123456784", "url": "https://wwwapps.ups.com/tracking/...", "company": "UPS" } ], "status": "IN_TRANSIT", "estimatedDeliveryAt": "2024-03-20T00:00:00Z", "deliveredAt": null } } ] } } } ] } } ``` **Common Scenarios:** **Scenario: Order Fulfilled & Shipped** ```json { "trackingInfo": [{"number": "9400...", "company": "USPS"}], "status": "IN_TRANSIT", "estimatedDeliveryAt": "2024-03-18T00:00:00Z" } ``` **Scenario: Order Delivered** ```json { "status": "DELIVERED", "deliveredAt": "2024-03-15T14:23:00Z" } ``` **Scenario: Not Yet Fulfilled** ```json { "fulfillmentOrders": { "edges": [ { "node": { "status": "OPEN", "fulfillments": {"edges": []} } } ] } } ``` **Scenario: No Order Yet (New Subscription)** Returns `null` or empty response **Important Considerations:** **Real-Time Shopify Query:** - Makes live call to Shopify GraphQL API - NOT cached data - Response time: 500-1500ms - Subject to Shopify rate limits **Data Freshness:** - Returns current fulfillment status from Shopify - Tracking updates reflect Shopify's data - May lag behind carrier's actual status - Shopify updates tracking periodically **Null Responses:** - Returns null if no orders exist yet - Returns null if order ID not found - Handle gracefully in UI **Multiple Fulfillments:** - Order may have multiple fulfillments (split shipments) - Each fulfillment has own tracking - Iterate through all fulfillment nodes **Integration Example:** **Customer Portal - Track Shipment:** ```javascript const order = await fetch( `/api/external/v2/subscription-contract-details/subscription-fulfillments/${contractId}`, { headers: { 'X-API-Key': 'your-key' } } ).then(r => r.json()); if (!order) { return '

No shipment information available yet.

'; } const fulfillments = order.fulfillmentOrders?.edges || []; fulfillments.forEach(fo => { fo.node.fulfillments?.edges?.forEach(f => { const tracking = f.node.trackingInfo?.[0]; if (tracking) { console.log(`Track with ${tracking.company}: ${tracking.number}`); console.log(`URL: ${tracking.url}`); } }); }); ``` **Best Practices:** 1. **Cache Response**: Cache for 30-60 minutes to reduce Shopify API calls 2. **Handle Nulls**: Always check for null/empty responses 3. **Parse GraphQL**: Navigate edges/nodes structure carefully 4. **Show All Tracking**: Display all fulfillments if multiple shipments 5. **Link to Carrier**: Provide clickable tracking URLs **Authentication:** Requires valid X-API-Key header # List quick checkout links Source: https://developers.appstle.com/subscription-admin-api/subscription-management/list-quick-checkout-links /subscription/admin-api-swagger.json get /api/external/v2/quick-checkout/links # Replace product variants in a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/replace-product-variants-in-a-subscription-contract /subscription/admin-api-swagger.json post /api/external/v2/subscription-contract-details/replace-variants-v3 Replaces existing product variants with new ones in a subscription contract. This endpoint supports both regular subscription products and one-time products, allowing you to swap products, update quantities, and manage the product mix in a subscription. **Key Features:** - **Bulk Replace**: Replace multiple products at once by providing lists of old and new variants - **Line-Specific Replace**: Target a specific line item using oldLineId for precise replacement - **Quantity Management**: Set new quantities for replaced products - **One-Time Products**: Add or remove one-time purchase products independently - **Discount Preservation**: Configurable discount carry-forward logic **Discount Carry Forward Options:** - **PRODUCT_THEN_EXISTING**: First tries to apply the new product's selling plan discount, then falls back to existing discount - **PRODUCT_PLAN**: Always uses the new product's selling plan discount - **EXISTING_PLAN**: Maintains the existing product's discount structure **Important Notes:** - At least one regular subscription product must remain in the contract - One-time products and free products don't count towards the minimum product requirement - The system automatically handles pricing adjustments based on billing/delivery intervals - Shipping prices are automatically recalculated after changes **Use Cases:** - Product upgrades/downgrades (e.g., switching coffee blend or size) - Quantity adjustments during product swap - Adding limited-time or seasonal products as one-time purchases - Bulk product replacements for subscription migrations **Authentication:** Requires valid X-API-Key header # Split or duplicate an existing subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/split-or-duplicate-an-existing-subscription-contract /subscription/admin-api-swagger.json post /api/external/v2/subscription-contract-details/split-existing-contract Creates a new subscription contract by either splitting (moving) or duplicating selected line items from an existing contract. This endpoint allows you to divide a subscription into multiple contracts or create a copy with specific products. **Split vs Duplicate Mode:** - **Split (isSplitContract=true)**: Moves the selected line items from the original contract to a new contract. The original contract will no longer contain these items. - **Duplicate (isSplitContract=false)**: Creates a new contract with copies of the selected line items while keeping them in the original contract. **Important Notes:** - When splitting, at least one subscription product must remain in the original contract - The new contract inherits all settings from the original: customer, payment method, billing/delivery policies, shipping address, and custom attributes - Billing schedule is automatically generated for the new contract - One-time products and free products don't count towards the minimum product requirement **Use Cases:** - Split a subscription when customer wants different delivery schedules for different products - Create a gift subscription from an existing subscription - Separate products for different shipping addresses - Duplicate a subscription for testing or backup purposes **Authentication:** Requires valid X-API-Key header # Update custom attributes on a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-custom-attributes-on-a-subscription-contract /subscription/admin-api-swagger.json post /api/external/v2/update-custom-note-attributes Updates or replaces custom key-value attributes on a subscription contract. These attributes are stored with the subscription and can be used to track custom data, preferences, or metadata that's important for your business processes. **Custom Attributes Overview:** Custom attributes are key-value pairs that allow you to store additional information on subscriptions. They are: - Visible in the Shopify admin and accessible via API - Included in order data when subscription orders are created - Preserved across subscription lifecycle events - Useful for integrations and custom workflows **Update Modes:** - **Merge Mode (overwriteExistingAttributes=false)**: Adds new attributes and updates existing ones with matching keys. Other attributes remain unchanged. - **Replace Mode (overwriteExistingAttributes=true)**: Completely replaces all existing attributes with the provided list. **Common Use Cases:** - Store gift messages or special instructions - Track referral sources or marketing campaigns - Add internal reference numbers or tracking codes - Store customer preferences or customization options - Integration data for third-party systems **Important Notes:** - Attribute keys should not conflict with Shopify's reserved attributes - Both keys and values are stored as strings - Changes are logged in the activity history - Invalid discount codes may be automatically removed during update **Authentication:** Requires valid X-API-Key header # Update delivery interval for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-delivery-interval-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-delivery-interval Updates the delivery interval for the specified subscription contract. This endpoint allows external API consumers to change the delivery interval count and type (of type SellingPlanInterval) used for scheduling deliveries. The service validates that the contract exists, that the new delivery interval differs from the current setting, and that it meets all business rules. Authentication is enforced via the X-API-Key header (the 'api_key' parameter is deprecated). # Update delivery price for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-delivery-price-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-delivery-price Updates the fixed delivery price for all future orders in a subscription contract. This allows manual override of calculated shipping rates with a custom delivery fee. **Key Features:** - Sets a fixed delivery price regardless of shipping calculations - Overrides any dynamic shipping rates from carriers - Applies to all future orders immediately - Can set to 0 for free delivery - Automatically handles invalid discount codes - Tracks price changes in activity log **How Delivery Pricing Works:** - **Calculated Rates**: Default behavior uses carrier rates and rules - **Manual Override**: This endpoint sets a fixed price - **Precedence**: Manual price overrides all calculations - **Currency**: Always in shop's base currency **Common Use Cases:** - **Free Shipping Promotions**: Set to 0.00 - **Flat Rate Shipping**: Fixed price regardless of location - **VIP Customer Rates**: Special shipping prices - **Subscription Perks**: Reduced delivery fees - **Price Corrections**: Fix shipping calculation errors - **Regional Adjustments**: Custom rates for specific areas **Impact on Orders:** - Next order uses new delivery price - All future recurring orders affected - Existing orders keep original pricing - Customer sees updated total immediately - No recalculation on address changes **Price Validation:** - Must be a valid decimal number - Can be 0 for free shipping - No maximum limit enforced - Negative values typically rejected by Shopify - Currency precision respected (e.g., 2 decimals for USD) **Side Effects:** - Invalid discount codes automatically removed - Activity log created with old/new prices - No customer email notification sent - Shipping tax recalculated if applicable - Total order value updated **Reverting to Calculated Rates:** To restore dynamic shipping calculations: 1. Use the sync shipping price endpoint 2. Or update delivery method 3. Manual price remains until explicitly changed **Important Notes:** - Price includes all delivery charges (shipping + handling) - Does not affect delivery method or speed - Consider customer communication for increases - May affect subscription profitability - Some payment methods may decline on total changes **Authentication:** Requires valid X-API-Key header # Update maximum cycles for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-maximum-cycles-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-max-cycles Updates the maximum number of billing cycles (orders) after which a subscription will automatically terminate. This creates a fixed-duration subscription that ends after a specific number of orders. **What are Maximum Cycles?** Maximum cycles define subscription duration limits where: - Subscription automatically cancels after the specified number of orders - No further orders are generated once maximum is reached - Useful for fixed-term offers, trials, or seasonal subscriptions - Customer is notified before final order (if configured) **Key Features:** - Cannot set below current cycle count (prevents immediate cancellation) - Setting to null creates an indefinite subscription - Automatically reschedules order queue based on new limit - Preserves all other subscription settings - Validates against current subscription progress **Current Cycle Calculation:** - Current cycle = Successful billing attempts + 1 - Failed payments don't count toward maximum - First order is cycle 1, second is cycle 2, etc. - System prevents setting max below current position **Common Use Cases:** - **Trial Subscriptions**: '3-box trial' that auto-ends - **Seasonal Programs**: '6-month summer subscription' - **Limited Series**: '12-issue magazine subscription' - **Promotional Offers**: 'First 5 boxes at special price' - **Gift Subscriptions**: Fixed duration gifts that don't renew **What Happens at Maximum:** When a subscription reaches its maximum cycles: 1. Final order is processed normally 2. Subscription status changes to CANCELLED 3. No future orders are scheduled 4. Customer receives cancellation notification 5. Cannot be reactivated (new subscription required) **Interaction with Min Cycles:** - Can have both min and max (e.g., 3 min, 12 max) - Min cycles enforces commitment period - Max cycles enforces termination - Common pattern: Commitment with defined end **Queue Management:** After updating max cycles: - System recalculates future order schedule - Removes orders beyond the new maximum - Adjusts upcoming order notifications - Updates subscription end date projections **Important Notes:** - Validation prevents accidental immediate cancellation - Changes apply to future billing cycles only - Activity log tracks old and new values - Consider customer communication for changes - Setting null removes any duration limit **Authentication:** Requires valid X-API-Key header # Update minimum cycles for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-minimum-cycles-for-a-subscription-contract /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-min-cycles Updates the minimum number of billing cycles (orders) that a customer must complete before they can cancel their subscription. This creates a commitment period that helps with customer retention and business predictability. **What are Minimum Cycles?** Minimum cycles represent a commitment period where: - Customers must complete a specified number of orders - Cancellation is blocked until the minimum is met - Often tied to special pricing or promotional offers - Counted from the subscription start date **Key Features:** - Updates commitment period for existing subscriptions - Can increase or decrease minimum cycles - Setting to null removes the minimum commitment - Preserves all other subscription settings - Automatically handles invalid discount codes - Updates future billing queue after change **Common Use Cases:** - **Promotional Offers**: '3-month minimum for 50% off' - **Hardware Subsidies**: '12-month commitment with free device' - **Loyalty Programs**: Reduce minimum after customer proves loyalty - **Seasonal Campaigns**: Temporary commitment requirements - **Contract Adjustments**: Customer service exceptions **Impact on Customers:** - Cannot cancel via portal until minimum cycles complete - Pause/resume typically still allowed (check settings) - Shows commitment status in customer portal - No automatic notification sent (consider sending separately) **Cycle Counting:** - Only successful billing attempts count toward minimum - Failed payments don't increment the cycle count - Skipped orders (if allowed) don't count - Current cycle = successful past orders + 1 **Interaction with Max Cycles:** - Min cycles must be less than or equal to max cycles - If max cycles exist, subscription auto-cancels after maximum - Common pattern: 3 min cycles, 12 max cycles **Best Practices:** - Clearly communicate commitment terms upfront - Consider grandfathering existing customers - Use reasonable minimums (typically 3-12 cycles) - Document reason for changes in activity logs - Send customer notification for transparency **Important Notes:** - Changes apply immediately to cancellation logic - Doesn't affect past or in-progress orders - Customer portal respects this setting automatically - Activity log tracks old and new values - Consider legal requirements in your jurisdiction **Authentication:** Requires valid X-API-Key header # Update order note for subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-order-note-for-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-order-note/{contractId} Updates the order note that will be added to all future orders generated by this subscription. Order notes help merchants track special instructions, customer preferences, or internal information about the subscription. **Key Features:** - Note is automatically added to all future recurring orders - Visible in both merchant portal and customer portal - Appears in Shopify order details for easy reference - Changes apply to future orders only (existing orders unchanged) **Common Use Cases:** - Customer preferences: "No nuts - severe allergy" - Delivery instructions: "Leave package at side door" - Gift messages: "Happy Birthday from Mom!" - Internal notes: "VIP customer - priority handling" - Processing instructions: "Include sample with each order" **Important Notes:** - Empty string clears the existing note - No character limit imposed by API (check Shopify limits) - HTML is not rendered - stored as plain text - Updates are logged in activity history - Note persists until explicitly changed **Order Generation:** When Appstle creates recurring orders: 1. Retrieves current order note from subscription 2. Adds note to new Shopify order 3. Note appears in order details immediately 4. Merchants can still edit individual order notes **Authentication:** Requires valid X-API-Key header # Update subscription contract status Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-contract-status /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-status Updates the status of a subscription contract to ACTIVE, PAUSED, or CANCELLED. This endpoint manages the lifecycle of subscriptions with automatic state tracking and notifications. **Status Transitions:** - **ACTIVE**: Resumes a paused subscription, enabling future billing and deliveries - **PAUSED**: Temporarily suspends all billing and deliveries until manually resumed - **CANCELLED**: Permanently terminates the subscription (irreversible) **Key Features:** - Validates status transitions (prevents same-status updates) - Tracks status change timestamps for audit trails - Sends automated email notifications to customers - Creates detailed activity logs for each status change - Handles concurrent modifications with automatic retry - Adjusts next billing date when resuming subscriptions **Permission Requirements (Customer Portal):** When called from customer portal context: - PAUSED status requires 'pauseResumeSub' permission - ACTIVE status (resuming) requires 'resumeSub' permission - CANCELLED status requires 'cancelSub' permission - External API calls bypass these permission checks **Status Change Side Effects:** - **Activating**: Recalculates next billing date, marks activation timestamp - **Pausing**: Stops all scheduled orders, marks pause timestamp - **Cancelling**: Terminates subscription permanently, marks cancellation timestamp **Error Recovery:** The system automatically handles: - Invalid discount codes by removing them and retrying - Concurrent modifications by retrying the operation - This ensures reliable status updates in production environments **Important Notes:** - Status values are case-insensitive - Cancelled subscriptions cannot be reactivated - Paused subscriptions retain all settings and can be resumed - Email notifications are sent automatically unless internally suppressed **Authentication:** Requires valid X-API-Key header # Update subscription delivery address and method Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-delivery-address-and-method /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-shipping-address Updates the shipping address or delivery method for a subscription contract. Supports standard shipping, local delivery, and pickup options with comprehensive address validation. **Delivery Method Types:** - **SHIPPING**: Standard shipping to customer address (default) - **LOCAL**: Local delivery within specified zip codes - **PICK_UP**: Customer pickup at designated location **Key Features:** - Zip code restrictions for customer portal access - Optional ShipperHQ address validation - Automatic phone number handling for local delivery - Email notifications to customers - Shipping price recalculation - Activity log tracking with old/new addresses **Zip Code Validation (Customer Portal Only):** When called from customer portal: - Standard shipping: Validates against 'allowToSpecificZipCode' setting - Local delivery: Validates against 'allowToSpecificZipCodeForLocalDelivery' setting - External API calls bypass zip code restrictions **Address Validation:** - If ShipperHQ is configured, addresses are validated for deliverability - Invalid addresses will return appropriate error messages - Helps prevent failed deliveries and shipping issues **Special Handling:** - Local delivery addresses missing phone numbers are auto-populated - Uses customer's phone if available, otherwise defaults to placeholder - Handles missing address components gracefully **Post-Update Actions:** - Sends 'SHIPPING_ADDRESS_UPDATED' email to customer - Creates activity log with address change details - Triggers asynchronous shipping price recalculation - May remove invalid discount codes automatically **Important Notes:** - Province codes should use ISO 3166-2 format (e.g., 'NY' for New York) - Country codes should use ISO 3166-1 alpha-2 format (e.g., 'US') - Phone numbers should include country code for international addresses - Pickup locations must be pre-configured in Shopify **Authentication:** Requires valid X-API-Key header # Update subscription delivery method Source: https://developers.appstle.com/subscription-admin-api/subscription-management/update-subscription-delivery-method /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-delivery-method Updates the delivery method for a subscription contract, supporting standard shipping, local delivery, and customer pickup options. The delivery method determines how future orders will be fulfilled. **Delivery Method Types:** The system automatically determines the type based on the subscription's current delivery address: - **SHIPPING**: Standard shipping to customer address (default) - **LOCAL**: Local delivery within merchant's delivery zones - **PICK_UP**: Customer pickup at designated location **Two Ways to Update:** 1. **Manual Parameters**: Provide title, code, and presentment title directly 2. **Delivery Method ID**: Provide a Shopify DeliveryMethodDefinition ID to auto-populate all fields **Using Delivery Method ID:** When providing a delivery-method-id: - System fetches the method from Shopify's delivery profiles - Automatically sets title, code, and presentment title - Applies associated delivery price if defined - ID can be numeric or full GraphQL ID format **Delivery Type Requirements:** - **Standard Shipping**: No additional requirements - **Local Delivery**: Phone number must exist in delivery address - **Pickup**: Pickup location must be configured in subscription **Price Updates:** - If using delivery-method-id with a fixed price, updates delivery price - Manual updates don't change the delivery price - Price changes affect all future orders **Side Effects:** - Creates activity log entry with method details - May remove invalid discount codes automatically - Updates apply to all future orders immediately - No customer notification sent **Common Use Cases:** - Switch from standard shipping to express shipping - Change pickup location for customer convenience - Update local delivery options based on availability - Apply seasonal delivery methods **Important Notes:** - Cannot change the delivery type (shipping/local/pickup) - only the method within that type - Delivery methods must be configured in Shopify's shipping settings - Changes don't affect orders already in fulfillment **Authentication:** Requires valid X-API-Key header # Add one-time product to subscription order Source: https://developers.appstle.com/subscription-admin-api/subscription-one-time-products/add-one-time-product-to-subscription-order /subscription/admin-api-swagger.json put /api/external/v2/subscription-contract-one-offs-by-contractId-and-billing-attempt-id **Discontinued.** One-time products are no longer applied to the generated order or cleaned up afterward — this endpoint now always returns 400 and creates nothing. Do not build new integrations against it. # Get all one-time products for a subscription contract Source: https://developers.appstle.com/subscription-admin-api/subscription-one-time-products/get-all-one-time-products-for-a-subscription-contract /subscription/admin-api-swagger.json get /api/external/v2/subscription-contract-one-offs-by-contractId Retrieves all one-time products (add-ons) associated with a specific subscription contract across all billing attempts. One-time products are additional items that customers can add to their subscription orders on a non-recurring basis. Each one-time product is tied to a specific billing attempt and will only be included in that particular order. **Key Features:** - Returns ALL one-time products across all queued billing attempts - Each product includes the billing attempt ID to identify which order it belongs to - Includes product details: variant ID, quantity, title, image, and price - Products are automatically removed after the associated billing attempt is processed **Use Cases:** - Display all one-time products added to a subscription across all upcoming orders - Allow customers to review their one-time add-ons in customer portal - Enable merchants to see all one-time products for a contract - Integrate with external systems to manage subscription add-ons **Authentication:** Requires valid X-API-Key header # Get one-time products for next upcoming order Source: https://developers.appstle.com/subscription-admin-api/subscription-one-time-products/get-one-time-products-for-next-upcoming-order /subscription/admin-api-swagger.json get /api/external/v2/upcoming-subscription-contract-one-offs-by-contractId Retrieves one-time products that will be included in the next scheduled order for a subscription contract. This endpoint specifically returns products associated with the earliest queued billing attempt, making it ideal for showing customers what additional items will be in their next delivery. **Key Differences from /subscription-contract-one-offs-by-contractId:** - Returns ONLY products for the next upcoming order (not all future orders) - Finds the earliest QUEUED billing attempt by date - Returns empty array if no queued billing attempts exist - Useful for "Your Next Order" previews in customer portals **Use Cases:** - Display a preview of the next order including one-time add-ons - Calculate the total cost of the upcoming delivery - Show customers their next delivery date with associated one-time items - Allow last-minute modifications before order processing **Business Logic:** 1. Searches for billing attempts with status = QUEUED 2. Selects the one with the earliest billing date 3. Returns all one-time products for that specific billing attempt 4. Returns empty array if no queued orders exist (e.g., subscription paused/cancelled) **Authentication:** Requires valid X-API-Key header that identifies the shop # Remove one-time product from subscription order Source: https://developers.appstle.com/subscription-admin-api/subscription-one-time-products/remove-one-time-product-from-subscription-order /subscription/admin-api-swagger.json delete /api/external/v2/subscription-contract-one-offs-by-contractId-and-billing-attempt-id Removes a previously added one-time product from a specific subscription order. This permanently deletes the one-time product from the specified billing attempt. The operation is idempotent - attempting to delete a non-existent product will succeed without error. **Important Notes:** - Only removes the product from the specified billing attempt - Cannot remove products from billing attempts that are already processed - Activity logs are created for audit trails - Returns the updated list of all one-time products for the contract **Use Cases:** - Allow customers to remove unwanted add-ons before order processing - Clean up cart-like functionality in customer portals - Programmatically manage one-time product selections - Implement "undo" functionality for product additions **Business Rules:** - Contract must belong to the authenticated shop - The specific combination of contractId + billingAttemptId + variantId identifies the product to remove - No error is thrown if the product doesn't exist (idempotent operation) **Authentication:** Requires valid X-API-Key header that identifies the shop # Send payment method update email to customer Source: https://developers.appstle.com/subscription-admin-api/subscription-payments/send-payment-method-update-email-to-customer /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-payment-method Triggers Shopify to send an email to the subscription customer with a secure link to update their payment method. This endpoint initiates Shopify's native payment update flow without requiring direct payment handling. **Key Features:** - Sends official Shopify payment update email to customer - Customer receives secure link to Shopify-hosted payment update page - No PCI compliance required - Shopify handles all payment data - Supports all Shopify-supported payment methods - Automatically updates subscription after customer completes the process **Process Flow:** 1. API call triggers email send request to Shopify 2. Shopify sends branded email to customer's registered email 3. Customer clicks secure link in email 4. Customer authenticates on Shopify-hosted page 5. Customer adds/updates payment method 6. Subscription automatically uses new payment method **Email Details:** - Sent from Shopify's email servers - Uses store's configured sender email - Subject line typically: "Update your payment method" - Contains secure, time-limited link - Shopify-branded with store information **Use Cases:** - Failed payment recovery - Expiring credit card updates - Customer-requested payment changes - Proactive payment method maintenance **Important Notes:** - Customer must have valid email address - Email cannot be customized (Shopify template) - Link expiration time set by Shopify - Multiple emails can be sent if needed - No webhook for completion - poll contract for updates **Rate Limiting:** - Subject to Shopify's email sending limits - Recommended: Wait 24 hours between sends to same customer - Excessive sends may be blocked by Shopify **Authentication:** Requires valid X-API-Key header # Update subscription payment method Source: https://developers.appstle.com/subscription-admin-api/subscription-payments/update-subscription-payment-method /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-existing-payment-method Updates the payment method for an existing subscription contract to use a different existing payment method. The new payment method must already be associated with the customer in Shopify. **Important Notes:** - The payment method must already exist in the customer's Shopify payment methods - Only valid, non-revoked payment methods can be used - The update is processed through Shopify's subscription draft system - Payment method details are cached locally after successful update **Process Flow:** 1. Validates subscription contract exists 2. Creates a draft update in Shopify 3. Updates the draft with new payment method 4. Commits the draft to apply changes 5. Caches payment instrument details locally 6. Records activity log entry **Authentication:** Requires valid X-API-Key header # Add products or variants to an existing subscription group Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/add-products-or-variants-to-an-existing-subscription-group /subscription/admin-api-swagger.json put /api/external/v2/subscription-groups/{id}/add-products Adds one or more products and/or product variants to an existing subscription group (selling plan group). This endpoint provides a simple way to expand the product catalog eligible for subscription without modifying the group's configuration or existing product assignments. **Key Features:** - Add multiple products and variants in a single request - Preserves existing product and variant assignments - Maintains the order of products as provided - Updates delivery profiles automatically if needed - Synchronously updates Shopify and returns the updated group - Optional response enhancement with added products information **Important Notes:** - Product IDs should be numeric Shopify product IDs without the gid:// prefix - Variant IDs should be numeric Shopify variant IDs without the gid:// prefix - Adding products also makes all their variants eligible for subscription - Adding specific variants limits subscription eligibility to those variants only - Duplicate products/variants are handled gracefully by Shopify - URL length limits apply to the comma-separated lists (use bulk endpoint for large additions) - Include 'x-return-short-response: true' header to get only added products/variants information in response **Use Cases:** - Launch new products with subscription options - Add seasonal items to existing subscription groups - Enable subscription for specific product variants - Expand subscription eligibility without changing plan configurations - Track which products were added in the current request **Authentication:** Requires valid X-API-Key header # Add products or variants to an existing subscription group (async) Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/add-products-or-variants-to-an-existing-subscription-group-async /subscription/admin-api-swagger.json post /api/external/v2/subscription-groups/v3/{id}/add-products Same JSON body as the v2 bulk-add-products endpoint, but returns immediately with a 202 and processes the update in the background via an AWS Step Function worker instead of within the HTTP request. Use this for large batches (hundreds to thousands of IDs) that would otherwise risk hitting request/gateway timeouts. Poll GET /api/external/v2/subscription-groups/v3/{id}/add-products/status to check progress. Only one such job may run per shop at a time; a second request while one is in progress is rejected with a 400. **Authentication:** Requires valid X-API-Key header # Bulk add products or variants to an existing subscription group Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/bulk-add-products-or-variants-to-an-existing-subscription-group /subscription/admin-api-swagger.json post /api/external/v2/subscription-groups/{id}/bulk-add-products Adds multiple products and/or product variants to an existing subscription group (selling plan group) using a JSON request body. This endpoint is ideal for adding large numbers of products/variants that would exceed URL length limits in the query parameter version. **Advantages over query parameter endpoint:** - No URL length restrictions - Cleaner syntax for large lists - Better for programmatic integrations - Supports the same functionality with improved scalability **Behavior:** - Identical to the query parameter endpoint in functionality - Adds products/variants to existing assignments - Preserves product order - Updates synchronously and returns the updated group — for very large batches (thousands of IDs) that risk request/gateway timeouts, use the async v3 endpoint (POST /api/external/v2/subscription-groups/v3/{id}/add-products) instead **Authentication:** Requires valid X-API-Key header # Check status of an async add-products job Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/check-status-of-an-async-add-products-job /subscription/admin-api-swagger.json get /api/external/v2/subscription-groups/v3/{id}/add-products/status Polls the status of a bulk add-products job started via POST .../add-products (v3 async endpoint). Only one such job runs per shop at a time; check 'subscriptionGroupId' and 'matchesRequestedGroup' in the response to confirm it's reporting on the group you submitted, in case a newer job for a different group has since started. **Authentication:** Requires valid X-API-Key header # Create a new subscription group (selling plan group) Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/create-a-new-subscription-group-selling-plan-group /subscription/admin-api-swagger.json post /api/external/v2/subscription-groups Creates a new subscription group with one or more selling plans in Shopify. A subscription group is a container for selling plans that can be applied to products and variants. Each group can contain multiple plans with different frequencies, discounts, and delivery schedules. **Key Features:** - Create multiple subscription plans within a single group - Configure complex discount tiers (up to 2 levels + custom cycles) - Set up free trials with automatic discount transitions - Define prepaid plans (PAY_AS_YOU_GO_PREPAID) with custom billing cycles - Configure member-only plans with tag-based access control - Set specific delivery dates (e.g., 15th of every month) - Add all products from store or specific collection automatically **Discount Configuration:** - First discount tier: Applied from a specific cycle (afterCycle1) - Second discount tier: Applied from another cycle (afterCycle2) - Additional cycles: Configure via appstleCycles for complex scenarios - Free trials: Automatically configure 100% discount for trial period **Product Assignment:** - Assign specific products via productIds field in request body - Assign specific variants via variantIds field in request body - Use isAddAllProduct=true to add all store products - Use collectionId with isAddAllProduct=true to add all products from a collection **Authentication:** Requires valid X-API-Key header # Get all selling plans across all subscription groups Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-selling-plans-across-all-subscription-groups /subscription/admin-api-swagger.json get /api/external/v2/subscription-groups/all-selling-plans Retrieves a flattened list of all selling plans from all subscription groups in the store. This endpoint provides a consolidated view of every subscription plan available, regardless of which group it belongs to. **Response includes:** - All selling plans with their configurations - Group information for each plan (groupId and groupName) - Complete discount and pricing details - Delivery and billing frequencies - Member restrictions and settings - Free trial configurations **Use Cases:** - Build a unified subscription selector - Compare all available subscription options - Analyze pricing across all plans - Find specific plan configurations - Generate reports on subscription offerings **Differences from /subscription-groups endpoint:** - Returns plans as a flat list, not grouped - Each plan includes its parent group information - Easier to search/filter across all plans - Does not include product/variant assignments **Authentication:** Requires valid X-API-Key header # Get all subscription groups Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/get-all-subscription-groups /subscription/admin-api-swagger.json get /api/external/v2/subscription-groups Retrieves a list of all subscription groups (selling plan groups) configured in the store. This endpoint provides a complete overview of all subscription offerings available. **Response includes:** - All subscription groups with their configurations - Complete selling plan details for each group - Product and variant assignments (as JSON strings) - Discount configurations and tiers - Member restrictions and settings **Use Cases:** - Display all subscription options in a custom interface - Audit subscription configurations - Export subscription data for analysis - Synchronize with external systems **Performance Notes:** - Returns all groups in a single response (no pagination) - Response size grows with number of groups and plans - Product/variant lists are JSON-encoded strings within the response **Authentication:** Requires valid X-API-Key header # Get subscription group by ID Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/get-subscription-group-by-id /subscription/admin-api-swagger.json get /api/external/v2/subscription-groups/{id} Retrieves detailed information about a specific subscription group (selling plan group) by its ID. This endpoint provides complete configuration details for a single subscription group. **Response includes:** - Group name and configuration - All selling plans with complete details - Product and variant assignments - Discount tiers and configurations - Free trial settings - Member restrictions - Delivery and billing frequencies **Use Cases:** - Display detailed subscription options for editing - Verify configuration before updates - Debug subscription issues - Integration with external systems **Authentication:** Requires valid X-API-Key header # Remove products or variants from a subscription group Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/remove-products-or-variants-from-a-subscription-group /subscription/admin-api-swagger.json put /api/external/v2/subscription-groups/{id}/remove-products Removes specified products and/or variants from an existing subscription group (selling plan group). This endpoint allows selective removal of products without affecting the group's configuration or other product assignments. **Key Features:** - Remove multiple products and variants in a single request - Remove all products from a specific collection - Silently ignores products/variants not currently in the group - Preserves subscription group configuration and settings - Updates synchronously and returns the modified group **Collection Removal:** - When collectionId is provided, fetches and removes all products from that collection - Collection processing is limited to 5000 products maximum - Collection removal is additive to any explicitly specified productIds/variantIds **Important Notes:** - Removing products does NOT affect existing active subscriptions - Can remove all products, leaving an empty subscription group - Products/variants not in the group are silently ignored - Use numeric IDs without gid:// prefix for products and variants **Authentication:** Requires valid X-API-Key header # Remove products or variants from ALL subscription groups Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/remove-products-or-variants-from-all-subscription-groups /subscription/admin-api-swagger.json put /api/external/v2/subscription-groups/remove-products Removes specified products and/or variants from ALL subscription groups in the store. This is a powerful bulk operation that affects every subscription group simultaneously. **⚠️ WARNING:** This operation: - Affects ALL subscription groups in your store - Cannot be easily undone - Processes synchronously (may timeout for stores with many groups) - Does NOT affect existing active subscriptions **Use Cases:** - Discontinuing products from all subscription offerings - Removing seasonal items from all groups - Cleaning up deleted products from subscription groups - Compliance-driven product removals **Important Considerations:** - Each group is updated individually - If any group update fails, previous groups remain updated - Large operations may take significant time - Consider using individual group removal for better control **Authentication:** Requires valid X-API-Key header # Update subscription group details and product assignments Source: https://developers.appstle.com/subscription-admin-api/subscription-plans/update-subscription-group-details-and-product-assignments /subscription/admin-api-swagger.json put /api/external/v2/subscription-groups Updates an existing subscription group (selling plan group) including its name, selling plans configuration, and optionally manages product/variant assignments. This endpoint provides comprehensive update capabilities for both the subscription group structure and its product associations. **Key Capabilities:** - Update subscription group name - Modify existing selling plan configurations (discounts, frequencies, trials, etc.) - Add or remove products/variants through updateProducts and deleteProducts fields - Cannot add or remove selling plans - only modify existing ones - Automatically handles complex discount transitions for free trials and prepaid plans - Updates delivery profile associations if changed - Triggers asynchronous operations for metafield updates and free product checks **Important Notes:** - The 'id' field is required and must match an existing subscription group - For each selling plan in subscriptionPlans array, provide 'idNew' with the existing selling plan ID - Plan modifications maintain the same selling plan ID in Shopify - Product/variant updates are processed after group details are updated - Large product updates (allProduct=true) run asynchronously **Selling Plan Updates:** - Can change discount amounts, types, and cycles - Can modify billing/delivery frequencies - Can add/remove free trials - Can change member restrictions and tags - Can update specific day delivery settings - Can modify min/max cycles **Authentication:** Requires valid X-API-Key header # Add multiple products to subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/add-multiple-products-to-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-add-line-items Adds multiple product line items to an existing subscription contract in a single request. This batch operation is more efficient than making multiple individual requests and ensures all products are added with consistent processing. **Key Features:** - Batch addition of multiple products in one API call - All products are added as recurring items (not one-time) - Automatic quantity defaulting (0 or null becomes 1) - Sequential processing with full validation for each item - Returns final subscription state after all additions - Same pricing and discount logic as single-item endpoint **Processing Behavior:** - Products are added sequentially, not in parallel - If any product fails, previous additions remain - Each product goes through full validation - Existing product handling follows store settings - Activity logs created for each product added **Duplicate Product Handling:** If a product already exists in the subscription: - With 'updateExistingQuantityOnAddProduct' enabled: Updates quantity - Without setting: Adds as new line item - Setting applies to all products in the batch **Pricing and Discounts:** Each product receives: - Appropriate selling plan assignment - Discount carry-forward based on store settings - Currency and country-specific pricing - Fulfillment frequency multiplier application **Build-a-Box Validation:** - Applied if any product has _bb_id attribute - Validates total quantity after all additions - May fail entire batch if limits exceeded **Post-Processing:** After all products are added: - Build-a-Box discounts recalculated once - Product discounts synchronized - Single email notification sent - Shipping price updated once **Important Notes:** - Maximum recommended batch size: 10-20 products - Larger batches may timeout - All products become recurring items - Use single-item endpoint for one-time products **Authentication:** Requires valid X-API-Key header # Add product to subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/add-product-to-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-add-line-item Adds a new product line item to an existing subscription contract. Can add either recurring products that will appear in each order or one-time products that appear only in the next order. **Key Features:** - Supports both recurring and one-time product additions - Automatically applies appropriate pricing policies based on discount carry-forward settings - Handles duplicate products by updating quantity (if enabled) or adding as new line - Calculates prices based on billing/delivery frequency multipliers - Sends email notifications to customers (unless suppressed) - Creates activity logs for audit trail **Pricing Policy Application:** The system applies discounts based on the store's discount carry-forward setting: - **PRODUCT_PLAN**: Uses the discount from the product's selling plan - **EXISTING_PLAN**: Applies the discount structure from existing subscription items - **PRODUCT_THEN_EXISTING**: First attempts product plan, falls back to existing if not found **One-Time Products:** Products added with `isOneTimeProduct=true` will: - Only appear in the next scheduled order - Be automatically removed after fulfillment - Still attempt to match with appropriate selling plans - Can have subscription discounts applied if store setting allows **Duplicate Product Handling:** When adding a product that already exists in the subscription: - If store has 'updateExistingQuantityOnAddProduct' enabled and product is not one-time: Updates existing quantity - Otherwise: Adds as a new line item **Price Calculation:** - Base price is fetched considering contract currency and delivery country - Price is multiplied by fulfillment frequency ratio (billing interval / delivery interval) - Example: Monthly billing with weekly delivery = price × 4 **Authentication:** Requires valid X-API-Key header # Add product with custom price to subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/add-product-with-custom-price-to-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contract-add-line-item Adds a new product line item with a custom price override to an existing subscription contract. This endpoint bypasses standard pricing and selling plans to set an exact price. **Key Features:** - Direct price control - set any price regardless of product pricing - Optional automatic discount application based on shop settings - No selling plan assignment - pure custom pricing - Supports both recurring and one-time products - Email notifications sent to customers - Activity logging for audit trails **Custom Pricing Behavior:** - Price is set exactly as provided (no calculations) - Overrides any product-level pricing - Not affected by currency or country - No automatic discounts from selling plans - Price remains fixed unless manually updated **Shop-Level Discount Application:** Despite the method name, this endpoint MAY apply discounts: - Checks shop setting: 'applySubscriptionDiscount' - If enabled, applies shop's default subscription discount - Discount type: PERCENTAGE or FIXED (per shop config) - Creates manual discount: 'PRODUCT_DISCOUNT_[lineId]' - Discount is additional to the custom price **Common Use Cases:** - **Negotiated Pricing**: Custom rates for specific customers - **Price Matching**: Match competitor pricing - **Promotional Pricing**: Special one-off prices - **Bundle Pricing**: Custom prices for package deals - **Legacy Pricing**: Honor old pricing agreements - **Test Orders**: Zero or nominal pricing for testing **Differences from Standard Add:** - No selling plan association - No pricing policy rules - No automatic tier discounts - Price doesn't adjust with frequency changes - Manual price management required **One-Time Products:** When `isOneTimeProduct=true`: - Product appears only in next order - Automatically removed after fulfillment - Custom price applies to that single order - Shop discount still applies if configured **Price Validation:** - Must be a positive number - No maximum limit enforced - Can be less than product cost (loss leader) - Currency matches shop's base currency - Decimal precision per currency rules **Important Considerations:** - No automatic price updates with product changes - Frequency changes don't affect pricing - May bypass minimum order values - Consider margin implications - Document reason for custom pricing **Post-Addition Effects:** - Customer receives product addition email - Activity log records custom price - Shipping may need recalculation - Order total updates immediately - No impact on other line items **Authentication:** Requires valid X-API-Key header # Remove multiple products from subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/remove-multiple-products-from-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-remove-line-items Removes multiple product line items from an existing subscription contract in a single request. Products are removed sequentially, allowing partial success if errors occur. **Key Features:** - Sequential processing - items removed one by one - Partial success allowed - previous removals persist if later ones fail - Validates each removal against current subscription state - Smart discount cleanup for line-specific discounts - Comprehensive validation for each product removal - Triggers all side effects after each successful removal **Processing Order Matters:** Items are removed in the order provided: 1. First item validated and removed 2. Second item validated against updated state 3. Continue until all processed or error occurs 4. Returns final subscription state **Validation Per Item:** - Line item must exist in subscription - At least one recurring product must remain - Build-a-Box min/max quantities maintained - Minimum cycle commitments honored - One-time and free products don't count toward minimum **Discount Handling:** When `removeDiscount=true`: - Removes discounts applied ONLY to the removed line - Preserves discounts that apply to multiple lines - Each removal evaluates discounts independently - Build-a-Box discounts auto-adjust based on quantity **Side Effects Per Removal:** Each successful removal triggers: - Activity log entry created - Customer email sent - Shipping price recalculation - Build-a-Box discount resync - Product discount adjustments **Partial Success Scenarios:** If removing 3 items and the 2nd fails: - 1st item: Successfully removed - 2nd item: Fails, error returned - 3rd item: Not attempted - Result: Subscription with 1st item removed **Performance Considerations:** - Each removal is a separate Shopify API transaction - Multiple emails may be sent to customer - Consider using single remove for 1-2 items - Large batches may timeout - Recommended max: 5-10 items per request **Build-a-Box Handling:** For Build-a-Box subscriptions: - Validates total quantity after EACH removal - May fail mid-batch if minimum breached - Discounts recalculate after each item - Consider removal order carefully **Important Notes:** - NOT atomic - no rollback on partial failure - Order of line IDs affects success probability - Customer receives email per removed item - All validations apply as if removing individually - Response shows final state after all attempts **Authentication:** Requires valid X-API-Key header # Remove product from subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/remove-product-from-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-remove-line-item Removes a specific product line item from an existing subscription contract. Can optionally retain prorated discounts associated with the removed product. **Key Features:** - Validates minimum subscription requirements before removal - Handles discount cleanup for line-specific discounts - Enforces minimum cycle commitments on products - Supports Build-a-Box quantity validation - Automatic retry on concurrent modification conflicts - Email notifications for subscription changes **Validation Rules:** - At least one recurring subscription product must remain after removal - One-time products and free products don't count toward the minimum - Products with minimum cycle commitments cannot be removed until fulfilled - Build-a-Box subscriptions validate against minimum/maximum quantity rules **Discount Handling:** When `removeDiscount=true` (default): - Discounts applied only to the removed line item are deleted - Discounts applied to multiple line items are retained - System automatically identifies which discounts to remove **Retry Mechanism:** The endpoint automatically retries in these scenarios: - Concurrent modification detected (another process updated the subscription) - Invalid discount code errors (removes problematic discount and retries) - This ensures reliable operation in high-traffic environments **Post-Removal Actions:** - Activity log created for audit trail - Email notification sent to customer - Shipping price recalculated asynchronously - Build-a-Box discounts resynced if applicable - Product-specific discounts resynced **Authentication:** Requires valid X-API-Key header # Update line item pricing policy with cycle-based discounts Source: https://developers.appstle.com/subscription-admin-api/subscription-products/update-line-item-pricing-policy-with-cycle-based-discounts /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-pricing-policy Sets up advanced pricing rules for a subscription line item that change based on the number of successful orders (cycles). This powerful feature enables loyalty discounts, promotional pricing tiers, and subscribe-and-save models. **What are Pricing Policies?** Pricing policies define how a product's price changes over the subscription lifetime: - **Base Price**: Starting price for the product - **Cycle Discounts**: Price changes after specific numbers of orders - **Automatic Application**: System applies correct price based on order history **Supported Discount Types:** - **PERCENTAGE**: Percentage off base price (e.g., 10% off) - **FIXED**: Fixed amount off base price (e.g., $5 off) - **PRICE**: Override with new fixed price (e.g., $19.99) Note: SHIPPING and FREE_PRODUCT types are not supported by this endpoint **Cycle Counting:** - Cycles start at 1 (first order is cycle 1) - Only successful billing attempts count - Failed payments don't increment the cycle - Current cycle = 1 + count of successful past orders **Common Use Cases:** 1. **Subscribe & Save**: 10% off starting from 3rd order 2. **Loyalty Tiers**: 5% off after 3 orders, 10% after 6 orders 3. **Introductory Pricing**: First 2 orders at $9.99, then $14.99 4. **Promotional Periods**: Special price for orders 3-5 **Important Limitations:** - Maximum 2 cycle discounts per line item - Cycles must have different afterCycle values - Changes apply to future orders only - Existing orders keep their original pricing **Prepaid Subscription Handling:** For prepaid subscriptions (e.g., pay monthly for weekly delivery): - Base price is per delivery - Current price = base price × delivery frequency - Discounts apply to base price, then multiply **Side Effects:** - Sends price update email to customer - Recalculates Build-a-Box discounts - Updates product discount synchronization - Triggers shipping price recalculation - Creates detailed activity log **Authentication:** Requires valid X-API-Key header # Update multiple properties of a subscription line item Source: https://developers.appstle.com/subscription-admin-api/subscription-products/update-multiple-properties-of-a-subscription-line-item /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item Comprehensive endpoint for updating a subscription line item's quantity, price, product variant, and/or selling plan in a single API call. Changes are applied intelligently - only modified values trigger updates. **Key Features:** - Update any combination of: quantity, price, variant, or selling plan - Intelligent change detection - only updates what's different - Automatic handling of prepaid subscription pricing - Preserves existing discount cycles when updating price - Partial success allowed - some updates may fail while others succeed - Each change creates separate activity log entries **Prepaid Subscription Handling:** For prepaid subscriptions (billing interval > delivery interval): - When `isPricePerUnit=true`: Price is multiplied by interval ratio - Example: Monthly billing, weekly delivery = price × 4 - When `isPricePerUnit=false`: Price is used as total billing amount **Update Process:** 1. Validates line item exists in the subscription 2. Calculates interval multiplier for prepaid logic 3. Updates in order: selling plan → price → quantity → variant 4. Each update uses separate Shopify GraphQL mutation 5. Failures are logged but don't block other updates **Selling Plan Updates:** - Can update by ID or name (name takes precedence) - Only updates if different from current plan - Useful for changing delivery frequency options **Price Updates:** - Preserves existing discount cycles (up to 2 cycles) - Recalculates cycle discounts based on new base price - Triggers shipping price recalculation - Sends price update email to customer **Quantity Updates:** - Validates against min/max quantity rules - Updates Build-a-Box totals if applicable - May trigger discount recalculations **Variant Updates:** - Changes the product itself (different SKU) - Validates new variant exists and is available - May affect pricing and discounts **Important Notes:** - All parameters except contractId and lineId are optional - Provide only the values you want to change - Price is always in shop's base currency - Changes apply to future orders only **Authentication:** Requires valid X-API-Key header # Update product line item price Source: https://developers.appstle.com/subscription-admin-api/subscription-products/update-product-line-item-price /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-price Updates the base price of a specific product line item within a subscription contract. This endpoint intelligently handles pricing updates while preserving existing discount structures. **Key Features:** - Updates base price while maintaining discount cycles - Automatically calculates prepaid subscription prices - Preserves existing percentage/fixed discounts - Validates actual price changes before updating - Triggers shipping and discount recalculations - Sends price update notifications to customers **Base Price vs Current Price:** - **Base Price**: The unit price before any multipliers - **Current Price**: Base price × fulfillment multiplier - For monthly billing/weekly delivery: Current = Base × 4 - For pay-per-delivery: Current = Base × 1 **Discount Preservation:** By default, this endpoint preserves existing discount cycles: - Percentage discounts adjust to new base price - Fixed discounts remain at same dollar amount - Discount schedule (after X cycles) unchanged **Prepaid Subscription Handling:** For prepaid subscriptions (billing interval > delivery interval): - Automatically calculates fulfillment multiplier - Updates current price accordingly - Ensures correct billing amounts **Post-Update Actions:** 1. Build-a-Box discount recalculation 2. Product discount synchronization 3. Shipping price updates 4. Customer email notifications 5. Activity log creation **Important Notes:** - Price changes apply to future orders only - Cannot set price below $0.01 - Maximum 2 discount cycles preserved - No update if price unchanged **Authentication:** Requires valid X-API-Key header # Update product line item quantity in subscription Source: https://developers.appstle.com/subscription-admin-api/subscription-products/update-product-line-item-quantity-in-subscription /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-quantity Updates the quantity of a specific product line item within a subscription contract. This comprehensive operation handles quantity validation, discount recalculation, and special Build-a-Box constraints. **Key Features:** - Updates quantity for future orders only - Validates minimum/maximum quantities for line items - Special handling for Build-a-Box subscriptions - Automatic discount recalculation - Shipping price updates - Activity log tracking with old/new values **Build-a-Box (BAB) Validation:** For Build-a-Box subscriptions: - Enforces total minimum/maximum item counts - Only counts recurring products (excludes one-time and free items) - Validates across all BAB items in the subscription - Can be bypassed with 'allowToAddProductQuantityMinMaxReached' permission **Line Item Validation:** Individual products may have: - Minimum quantity requirements (min_quantity attribute) - Maximum quantity limits (max_quantity attribute) - These are enforced separately from BAB constraints **Post-Update Actions:** 1. **Activity Logging**: Records quantity change with old/new values 2. **BAB Discount Sync**: Recalculates Build-a-Box volume discounts 3. **Product Discount Sync**: Updates product-specific discounts 4. **Shipping Price Update**: Recalculates shipping based on new quantity **Discount Recalculation:** - Build-a-Box discounts adjust based on total quantity tiers - May remove old discount codes and apply new ones - Handles 'subscription contract has changed' retry scenarios **Important Notes:** - Line ID must be the full GraphQL ID format - Quantity must be positive (minimum 1) - Changes apply to all future orders - Past or in-progress orders are not affected **Authentication:** Requires valid X-API-Key header # Update selling plan for a subscription line item Source: https://developers.appstle.com/subscription-admin-api/subscription-products/update-selling-plan-for-a-subscription-line-item /subscription/admin-api-swagger.json put /api/external/v2/subscription-contracts-update-line-item-selling-plan Updates the selling plan associated with a specific product line item in a subscription contract. Selling plans define the delivery schedule, pricing rules, and billing policies for subscription products. **What are Selling Plans?** Selling plans are Shopify's way of defining subscription rules for products: - **Delivery Schedule**: How often the product is delivered (e.g., 'every 2 weeks', 'monthly') - **Pricing Policy**: Discounts and pricing tiers (e.g., '10% off', 'first order free') - **Billing Policy**: When and how often customer is charged - **Plan Name**: Customer-facing description like 'Deliver every month' **Key Features:** - Update by selling plan ID or name (name takes precedence if both provided) - Validates line item exists before attempting update - Only creates activity log if plan actually changes - Uses Shopify's draft system for safe updates - Preserves existing line item attributes and customizations **Common Use Cases:** - Change delivery frequency: Weekly → Monthly - Switch pricing tiers: Regular → VIP pricing - Update from old plan to new plan with better terms - Migrate products to new selling plan groups - Apply seasonal or promotional plans **Update Process:** 1. Validates subscription contract and line item exist 2. Creates a draft of the subscription contract 3. Updates the line item's selling plan in the draft 4. Commits the draft to apply changes 5. Records activity log if plan changed **Impact of Selling Plan Changes:** - **Delivery Schedule**: Next and future orders follow new schedule - **Pricing**: New plan's pricing applies from next billing - **Discounts**: Plan-specific discounts are recalculated - **Billing**: Billing frequency may change with the plan **Finding Selling Plans:** To find available selling plans: 1. Check product's selling plan groups in Shopify admin 2. Use Shopify's GraphQL API to query selling plans 3. Plans must be active and associated with the product **Important Notes:** - Both sellingPlanId and sellingPlanName are optional, but at least one required - If both provided, both are updated (name doesn't override ID) - Plan must be valid for the product variant - Changes apply to future orders only - No customer notification sent (consider sending separately) **Authentication:** Requires valid X-API-Key header # Calculate refund amount preview Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/calculate-refund-amount-preview /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-billing-attempts/refund-preview/{id} Calculates the refund amount that would be issued if a specific subscription order fulfillment were refunded. This provides a preview without actually processing the refund. **What it calculates:** - Total refund amount (order amount minus processing fees) - Restocking fees (if applicable) - Gateway fees that cannot be refunded - Net refund amount customer will receive **Use Cases:** - Show customer how much they'll get back before confirming refund - Display refund breakdown in customer portal - Validate refund eligibility **Important Notes:** - This is a preview only - does not process refund - Refund amount may vary based on payment gateway - Some gateways don't refund processing fees - Refunds can only be issued for fulfilled orders **Authentication:** Customer must be logged in and own the subscription # Get backup payment recovery opt-in status Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-backup-payment-recovery-opt-in-status /subscription/storefront-api-swagger.json get /subscriptions/cp/api/backup-payment-opt-in # Get past orders Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-past-orders /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-billing-attempts/past-orders Retrieves paginated list of past (processed) billing attempts for a subscription contract or customer. Includes successful orders, failed attempts, and skipped orders. **Filter Options:** - By contract ID: Get order history for specific subscription - By customer ID: Get all orders across all customer subscriptions - Pagination: Control page size and page number **Order Information:** - Billing attempt status (SUCCESS, FAILED, SKIPPED) - Shopify order ID for successful attempts - Billing date and processing date - Order total and line items - Error messages for failed attempts **Authentication:** Requires valid X-API-Key header # Get past orders report with detailed filtering Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-past-orders-report-with-detailed-filtering /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-billing-attempts/past-orders/report Retrieves a detailed report of past billing attempts with advanced filtering options. This endpoint provides comprehensive data for analytics, troubleshooting, and reporting purposes. **Filter Options:** - By status: SUCCESS, FAILED, SKIPPED - By date range: Filter by billing date range - By attempt count: Filter by number of retry attempts - By contract status: Filter by subscription contract status - By contract ID: Get report for specific subscription **Response Includes:** - Detailed billing attempt information - Shopify exception details for failed attempts (optional) - Retry attempt count - Processing timestamps - Order totals and line items **Authentication:** Requires valid X-API-Key header # Get subscription payment info for a contract Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-subscription-payment-info-for-a-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-payment-infos/{contractId} # Get upcoming orders (top orders) Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-upcoming-orders-top-orders /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-billing-attempts/top-orders Retrieves upcoming (queued) billing attempts for a subscription contract or customer. Returns the next scheduled orders that have not yet been processed. **Query Options:** - Filter by contract ID to see upcoming orders for a specific subscription - Filter by customer ID to see all upcoming orders for a customer - Results are ordered by billing date (earliest first) **Use Cases:** - Display "Your Next Order" in customer portal - Show upcoming delivery schedule - Calculate upcoming charges - Preview next order contents **Authentication:** Requires valid X-API-Key header # Get URL for 3DS / SCA security challenge Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/get-url-for-3ds-sca-security-challenge /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-billing-attempts/security-challenge-action-url/{id} # Process refund for subscription order Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/process-refund-for-subscription-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/refund-fulfillment/{id} Processes a refund for a fulfilled subscription order. This creates a refund in Shopify and returns the money to the customer's original payment method. **What it does:** 1. Validates order is eligible for refund (fulfilled, not already refunded) 2. Creates refund transaction in Shopify 3. Processes refund with payment gateway 4. Updates order status to 'Refunded' 5. Sends refund confirmation email to customer 6. Logs refund activity **Refund Eligibility:** - Order must be fulfilled - Cannot already be refunded - Must be within merchant's refund policy window - Payment gateway must support refunds **Refund Processing:** - Refund is processed to original payment method - Takes 5-10 business days to appear in customer's account - Processing fees are typically not refunded - Partial refunds are supported (if configured) **Important Warnings:** - This action cannot be undone - Refunded orders cannot be un-refunded - Inventory is restocked automatically - Refunds may incur gateway fees **Authentication:** Customer must be logged in and own the subscription # Recover a billing attempt stuck in progress Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/recover-a-billing-attempt-stuck-in-progress /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/fix-billing-attempt-in-progress/{id} # Reschedule a billing attempt to a new date Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/reschedule-a-billing-attempt-to-a-new-date /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/reschedule-order/{id} Changes the scheduled billing date for a billing attempt. This allows customers to adjust when their next order will be processed. **Rescheduling Options:** - Move to earlier date (if allowed by shop settings) - Move to later date - Optionally reschedule all future orders by the same offset **Important Behaviors:** - Only QUEUED billing attempts can be rescheduled - New date must be in the future - Can affect future billing schedule if rescheduleFutureOrder is true - Activity logs are created for audit trail **Use Cases:** - Customer wants to delay next delivery - Customer wants to receive order earlier - Adjust delivery schedule to align with customer needs - Coordinate deliveries with customer vacation/travel **Authentication:** Requires valid X-API-Key header # Retry a failed billing attempt Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/retry-a-failed-billing-attempt /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/retry-failed-billing-attempt # Skip a failed billing attempt's order Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/skip-a-failed-billing-attempts-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/skip-failed-order # Skip a specific order Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/skip-a-specific-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/skip-order/{id} Skips a specific billing attempt by ID. The order will not be processed on its scheduled date. This is useful when customers want to skip a particular delivery without canceling their subscription. **Important Behaviors:** - Only QUEUED billing attempts can be skipped - Skipped orders remain in the system with SKIPPED status - Future orders are not affected - Can be unskipped before the scheduled date if needed - Activity logs are created for audit trail **Use Cases:** - Customer is on vacation - Customer has excess inventory - Temporary delivery pause for one cycle **Authentication:** Requires valid X-API-Key header # Skip the next upcoming order for a subscription Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/skip-the-next-upcoming-order-for-a-subscription /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/skip-upcoming-order Skips the next scheduled billing attempt for a subscription contract without requiring the billing attempt ID. Automatically finds and skips the earliest QUEUED billing attempt. **Convenience Feature:** - No need to know the specific billing attempt ID - Automatically finds the next order - Ideal for "skip next order" functionality in customer portals **Use Cases:** - Simple "skip next delivery" button in customer portal - Quick skip without looking up billing attempt details - One-click skip functionality **Authentication:** Requires valid X-API-Key header # Trigger immediate billing for an order Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/trigger-immediate-billing-for-an-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/attempt-billing/{id} Immediately processes a billing attempt, creating an order in Shopify. This bypasses the scheduled billing date and processes the order right away. **Important Notes:** - Requires shop permission 'enableImmediatePlaceOrder' - Only QUEUED billing attempts can be processed - Creates an actual order in Shopify - Charges the customer's payment method immediately - Cannot be undone once processed **Use Cases:** - Customer requests early delivery - Process order immediately after resolving payment issue - Manual order processing for special cases **Authentication:** Requires valid X-API-Key header and shop permission # Unskip a previously skipped order Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/unskip-a-previously-skipped-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts/unskip-order/{id} Reverses a skip action on a billing attempt. The order will be restored to QUEUED status and will be processed on its scheduled date. **Important Notes:** - Only works on billing attempts with SKIPPED status - Must be done before the scheduled billing date - Cannot unskip after the billing date has passed - Activity logs are created for audit trail **Use Cases:** - Customer changes their mind about skipping - Correct accidental skip actions - Restore delivery after resolving temporary issue **Authentication:** Requires valid X-API-Key header # Update backup payment recovery opt-in status Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/update-backup-payment-recovery-opt-in-status /subscription/storefront-api-swagger.json put /subscriptions/cp/api/backup-payment-opt-in # Update subscription billing attempt Source: https://developers.appstle.com/subscription-storefront-api/billing-&-payments/update-subscription-billing-attempt /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-billing-attempts Updates an existing subscription billing attempt. This endpoint allows modification of billing attempt details such as billing date, order note, and other attributes. **Important Notes:** - Only QUEUED billing attempts can be updated - Cannot update attempts that are already processed or failed - Billing attempt must belong to the authenticated shop **Authentication:** Requires valid X-API-Key header # Get a subscription bundling configuration by token Source: https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/get-a-subscription-bundling-configuration-by-token /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-bundlings/by-token/{token} # Get public Build-a-Box bundle details by handle (v3) Source: https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/get-public-build-a-box-bundle-details-by-handle-v3 /subscription/storefront-api-swagger.json get /subscriptions/cp/api/v3/subscription-bundlings/external/get-bundle/{handle} # Get subscription bundle settings by ID Source: https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/get-subscription-bundle-settings-by-id /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-bundle-settings/{id} # List single-product Build-a-Box bundlings for the shop Source: https://developers.appstle.com/subscription-storefront-api/build-a-box-&-bundles/list-single-product-build-a-box-bundlings-for-the-shop /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-bundlings/single-product # Get campaign landing details Source: https://developers.appstle.com/subscription-storefront-api/campaigns/get-campaign-landing-details /subscription/storefront-api-swagger.json get /subscriptions/cp/api/campaigns/{id}/landing # List applicable customer portal campaigns Source: https://developers.appstle.com/subscription-storefront-api/campaigns/list-applicable-customer-portal-campaigns /subscription/storefront-api-swagger.json get /subscriptions/cp/api/campaigns/applicable # List applicable customer portal campaigns for multiple contracts in one call Source: https://developers.appstle.com/subscription-storefront-api/campaigns/list-applicable-customer-portal-campaigns-for-multiple-contracts-in-one-call /subscription/storefront-api-swagger.json get /subscriptions/cp/api/campaigns/applicable-bulk # Redeem a campaign offer Source: https://developers.appstle.com/subscription-storefront-api/campaigns/redeem-a-campaign-offer /subscription/storefront-api-swagger.json post /subscriptions/cp/api/campaigns/{id}/redeem # Track a campaign banner event Source: https://developers.appstle.com/subscription-storefront-api/campaigns/track-a-campaign-banner-event /subscription/storefront-api-swagger.json post /subscriptions/cp/api/campaigns/{id}/track # Get subscriptionscpapicancellation flowspublished Source: https://developers.appstle.com/subscription-storefront-api/cancellation-flow-customer-portal-resource/get-subscriptionscpapicancellation-flowspublished /subscription/storefront-api-swagger.json get /subscriptions/cp/api/cancellation-flows/published # Check Customer Account API authentication status Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/check-customer-account-api-authentication-status /subscription/storefront-api-swagger.json get /subscriptions/cp/api/customer-account-api/status Checks whether the current customer has valid Customer Account API tokens stored. Used by the customer portal to determine if the customer needs to authenticate. **Use Cases:** - Check if customer is authenticated before making Customer Account API GraphQL calls - Determine whether to show 'Connect Account' button in UI - Validate token validity before attempting sensitive operations **Response:** Returns authentication status and customer ID. **Authentication:** Customer must be logged in via Shopify customer session # Get shop info for the currently signed-in shop Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/get-shop-info-for-the-currently-signed-in-shop /subscription/storefront-api-swagger.json get /subscriptions/cp/api/shop-infos-by-current-login # Handle OAuth callback from Shopify Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/handle-oauth-callback-from-shopify /subscription/storefront-api-swagger.json get /subscriptions/cp/api/customer-account-api/oauth/callback OAuth 2.0 callback endpoint that receives the authorization code from Shopify after customer authorization. This endpoint is called automatically by Shopify after the customer authorizes the app. **Flow:** 1. Shopify redirects customer here with authorization code and state 2. Validates state parameter to prevent CSRF 3. Exchanges authorization code for access token using PKCE verifier 4. Validates ID token (JWT) from Shopify 5. Stores access token and refresh token securely 6. Redirects customer back to original return URL **Security:** - Validates state parameter matches stored value - Uses PKCE code verifier to exchange authorization code - Validates ID token signature and claims - State expires after 10 minutes **Error Handling:** - If customer denies authorization, redirects with error parameter - If token exchange fails, redirects with error parameter - All errors are logged for debugging **Note:** This endpoint should not be called directly - it's invoked by Shopify's OAuth redirect. # Initiate Customer Account API OAuth flow Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/initiate-customer-account-api-oauth-flow /subscription/storefront-api-swagger.json post /subscriptions/cp/api/customer-account-api/initiate Initiates the OAuth 2.0 authorization flow for Shopify's Customer Account API. This endpoint is used when a customer wants to grant the subscription app access to their Shopify customer account data. **What is Customer Account API?** Shopify's Customer Account API allows apps to access customer data (orders, addresses, payment methods) on behalf of the customer. This requires customer consent through an OAuth flow. **How it works:** 1. Customer portal calls this endpoint with a return URL 2. Backend generates PKCE challenge and state parameter 3. Returns authorization URL to redirect customer to Shopify 4. Customer authorizes on Shopify 5. Shopify redirects back to callback endpoint with authorization code 6. Callback endpoint exchanges code for access token **Important Notes:** - Requires customer to be logged in to the Shopify store - Only works with stores that have 'New Customer Accounts' enabled - Uses PKCE (Proof Key for Code Exchange) for security - State parameter prevents CSRF attacks - Access tokens are stored securely and used for subsequent Customer Account API calls **Authentication:** Customer must be logged in via Shopify customer session # Logout from Customer Account API Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/logout-from-customer-account-api /subscription/storefront-api-swagger.json get /subscriptions/cp/api/customer-account-api/logout Logs out the customer from the Customer Account API session. This can be initiated either by the customer clicking logout in the customer portal, or by Shopify's end session callback. **What it does:** - Deletes stored access and refresh tokens - Initiates Shopify's end session flow (if tokens available) - Redirects to return URL or back to customer portal **Two scenarios:** 1. **App-initiated logout**: Customer clicks logout in portal - Portal calls this endpoint with return URL - Tokens deleted, redirects to Shopify end session endpoint - Shopify redirects back to return URL 2. **Shopify-initiated logout**: Customer logs out globally from Shopify - Shopify calls this endpoint with id_token_hint - Tokens deleted, returns success **Important:** - This only logs out from Customer Account API, not from Shopify customer account - Customer will need to re-authenticate to use Customer Account API features again - Does not affect regular customer portal access (subscription management) **Authentication:** Optional - can be called from Shopify without authentication # Proxy GraphQL queries to Shopify Customer Account API Source: https://developers.appstle.com/subscription-storefront-api/customer-portal/proxy-graphql-queries-to-shopify-customer-account-api /subscription/storefront-api-swagger.json post /subscriptions/cp/api/customer-account-api/graphql Executes GraphQL queries against Shopify's Customer Account API on behalf of the authenticated customer. This endpoint handles token management, refresh, and authentication automatically. **What you can query:** - Customer profile information - Order history and details - Saved addresses - Payment methods - Subscriptions (via Customer Account API schema) **Token Management:** - Automatically uses stored access token - Refreshes expired tokens automatically - Returns 401 if customer needs to re-authenticate **Example Queries:** ```graphql query { customer { id emailAddress { emailAddress } defaultAddress { address1 city } } } ``` **Authentication:** Customer must be logged in and have completed OAuth flow # Get cancellation management configuration Source: https://developers.appstle.com/subscription-storefront-api/customer-retention/get-cancellation-management-configuration /subscription/storefront-api-swagger.json get /subscriptions/cp/api/cancellation-managements/{id} # Get subscriptionscpapicustomer retention activitieslatest discount usage Source: https://developers.appstle.com/subscription-storefront-api/customer-retention/get-subscriptionscpapicustomer-retention-activitieslatest-discount-usage /subscription/storefront-api-swagger.json get /subscriptions/cp/api/customer-retention-activities/latest-discount-usage # Get win-back offer landing details Source: https://developers.appstle.com/subscription-storefront-api/customer-retention/get-win-back-offer-landing-details /subscription/storefront-api-swagger.json get /subscriptions/cp/api/win-back-campaigns/{id}/landing # Reactivate a subscription from a win-back offer Source: https://developers.appstle.com/subscription-storefront-api/customer-retention/reactivate-a-subscription-from-a-win-back-offer /subscription/storefront-api-swagger.json post /subscriptions/cp/api/win-back-campaigns/{id}/reactivate # Record a customer retention activity Source: https://developers.appstle.com/subscription-storefront-api/customer-retention/record-a-customer-retention-activity /subscription/storefront-api-swagger.json post /subscriptions/cp/api/customer-retention-activities # Get custom CSS for customer portal Source: https://developers.appstle.com/subscription-storefront-api/customization/get-custom-css-for-customer-portal /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-custom-csses/{id} Retrieves the custom CSS styling configuration for the customer portal. This endpoint returns all custom CSS rules that have been configured to customize the appearance, layout, and branding of the subscription customer portal. **What is Custom CSS?** Custom CSS allows merchants to fully customize the visual appearance of their customer portal beyond the basic theme settings. This enables complete brand alignment and creates a seamless experience that matches the merchant's main store design. **Custom CSS Capabilities:** - **Layout Customization**: - Modify page layouts and spacing - Adjust grid and flexbox configurations - Control responsive breakpoints - Customize navigation and sidebars - **Typography**: - Custom fonts and font families - Font sizes, weights, and line heights - Letter spacing and text transforms - Heading and paragraph styles - **Colors and Branding**: - Brand color palette application - Custom background colors and gradients - Button and link styling - Hover and focus states - Border colors and shadows - **Component Styling**: - Subscription card appearances - Form input styling - Button designs and interactions - Modal and dialog boxes - Navigation menus - Product images and thumbnails - **Advanced Features**: - CSS animations and transitions - Media queries for responsive design - Pseudo-elements and pseudo-classes - Custom icons using CSS - Transform and filter effects **CSS Structure:** The returned CSS includes: - Global styles for portal-wide consistency - Component-specific styles - Responsive design rules - Theme overrides - Custom animations - Print styles (optional) **Common CSS Selectors Available:** ```css /* Portal container */ .subscription-portal { } /* Subscription cards */ .subscription-card { } .subscription-card-header { } .subscription-card-body { } /* Buttons */ .btn-primary { } .btn-secondary { } .btn-cancel { } /* Forms */ .form-control { } .form-group { } .form-label { } /* Navigation */ .portal-nav { } .nav-item { } /* Product displays */ .product-item { } .product-image { } .product-title { } ``` **Use Cases:** - Apply custom branding to match main store design - Create unique visual experiences for different customer segments - Implement seasonal or promotional themes - Enhance mobile responsiveness - Add accessibility improvements (high contrast, larger fonts) - A/B test different portal designs - Integrate with design systems - Implement dark mode or theme switching **Important Notes:** - CSS is sanitized for security (XSS prevention) - Certain properties may be restricted for security reasons - External resources (fonts, images) must use HTTPS - CSS is cached for performance - changes may take a few minutes to propagate - Invalid CSS syntax is automatically filtered out - Some core portal elements have !important styles that cannot be overridden **Best Practices:** - Use specific selectors to avoid conflicts - Test across different browsers and devices - Keep CSS organized with comments - Use CSS variables for maintainability - Minify CSS for production performance - Consider accessibility in color choices (WCAG compliance) - Provide fallbacks for advanced CSS features **Security Considerations:** - CSS is sanitized to prevent code injection - External URLs are validated - JavaScript in CSS is blocked (e.g., expression(), behavior()) - Data URIs are validated for malicious content **Authentication:** Requires valid X-API-Key header # Get customer portal label translations for locale Source: https://developers.appstle.com/subscription-storefront-api/customization/get-customer-portal-label-translations-for-locale /subscription/storefront-api-swagger.json get /subscriptions/cp/api/label-translations/locale # Get customer portal settings Source: https://developers.appstle.com/subscription-storefront-api/customization/get-customer-portal-settings /subscription/storefront-api-swagger.json get /subscriptions/cp/api/customer-portal-settings/{id} Retrieves the customer portal configuration and settings for the authenticated shop. The customer portal is the self-service interface where subscribers can manage their subscriptions, update payment methods, modify delivery addresses, and more. **What is the Customer Portal?** The customer portal is a dedicated web interface that allows your subscribers to manage their subscription accounts independently. This reduces support burden and improves customer experience by enabling self-service subscription management. **Settings Returned:** - **Display Configuration**: - Portal theme and branding settings - Custom colors and logo - Layout preferences - Custom CSS selectors - **Feature Toggles**: - Enable/disable subscription pausing - Enable/disable order skipping - Enable/disable product swapping - Enable/disable frequency changes - Enable/disable quantity modifications - Enable/disable address editing - Enable/disable payment method updates - Enable/disable subscription cancellation - **Subscription Management Options**: - Maximum pause duration allowed - Minimum subscription duration requirements - Skip limits per billing cycle - Product swap availability - One-time product add-ons - **Communication Settings**: - Email notification preferences - Custom portal messaging - Support contact information - Help text and instructions - **Access Control**: - Portal authentication method - Password requirements - Magic link settings - Session duration - **Advanced Options**: - Custom domain configuration - Redirect URLs - Webhook endpoints - Analytics tracking settings **Use Cases:** - Display customer portal with correct branding and theme - Determine which features are available to subscribers - Build custom portal interfaces using your settings - Sync portal configuration across systems - Validate subscription management capabilities - Configure third-party integrations **Important Notes:** - Settings are shop-specific and unique per merchant - Some features may be restricted based on subscription plan - Changes to settings are reflected immediately in the portal - Custom CSS must be valid and secure - Portal URL is typically: shop-domain.com/apps/subscriptions **Common Configuration Scenarios:** **1. Standard Self-Service Portal:** - Allow pausing (up to 3 months) - Allow skipping (max 2 consecutive orders) - Allow frequency changes - Allow quantity updates - Allow address editing - Enable payment method updates - Enable cancellation with feedback **2. Locked-Down Portal (Minimal Self-Service):** - Disable pausing - Disable skipping - Disable cancellation (require support contact) - Allow address editing only - Allow payment method updates only **3. Full-Service Portal (Maximum Flexibility):** - Enable all subscription management features - Allow unlimited pauses and skips - Enable product swapping - Enable one-time add-ons - Allow subscription splitting/merging - Custom branding and domain **Authentication:** Requires valid X-API-Key header # Get shop customization CSS by category Source: https://developers.appstle.com/subscription-storefront-api/customization/get-shop-customization-css-by-category /subscription/storefront-api-swagger.json get /subscriptions/cp/api/shop-customizations/css/{category} # Get widget label translations for locale Source: https://developers.appstle.com/subscription-storefront-api/customization/get-widget-label-translations-for-locale /subscription/storefront-api-swagger.json get /subscriptions/cp/api/widget-label-translations/locale # Get a page of order events (paginated) Source: https://developers.appstle.com/subscription-storefront-api/delivery-&-shipping/get-a-page-of-order-events-paginated /subscription/storefront-api-swagger.json get /subscriptions/cp/api/orders/{orderId}/events # Get the live fulfillment status for an order Source: https://developers.appstle.com/subscription-storefront-api/delivery-&-shipping/get-the-live-fulfillment-status-for-an-order /subscription/storefront-api-swagger.json get /subscriptions/cp/api/orders/{orderId}/live-status # List delivery profile locations Source: https://developers.appstle.com/subscription-storefront-api/delivery-&-shipping/list-delivery-profile-locations /subscription/storefront-api-swagger.json get /subscriptions/cp/api/delivery-profiles/get-locations # Get available loyalty point redemption options Source: https://developers.appstle.com/subscription-storefront-api/loyalty-integration/get-available-loyalty-point-redemption-options /subscription/storefront-api-swagger.json get /subscriptions/cp/api/loyalty-integration/redeem-options Returns all available rewards that the customer can redeem their loyalty points for. This shows customers what they can spend their points on. **Common Redemption Options:** - Discount codes (e.g., $5 off for 500 points) - Percentage discounts (e.g., 10% off for 1000 points) - Free shipping rewards - Free products or samples - Exclusive access to sales **Response includes:** - Redemption option ID - Name and description - Points cost - Reward value (dollar amount or percentage) - Availability (minimum purchase, restrictions) - Whether customer has enough points **Filtering:** - Only shows active redemption options - Filters based on customer's tier/VIP level - Shows whether customer has sufficient points **Use Cases:** - Display 'Redeem Points' section in customer portal - Show available rewards in checkout - Encourage customers to save points for bigger rewards **Authentication:** Customer must be logged in via Shopify customer session # Get available ways to earn loyalty points Source: https://developers.appstle.com/subscription-storefront-api/loyalty-integration/get-available-ways-to-earn-loyalty-points /subscription/storefront-api-swagger.json get /subscriptions/cp/api/loyalty-integration/earn-options Returns all active point earning campaigns and rules that the customer can participate in. This shows customers how they can earn more points. **Common Earning Methods:** - Points per dollar spent on purchases - Bonus points for first subscription - Points for referring friends - Birthday bonus points - Social media follows (Instagram, Facebook, Twitter) - Product reviews - Account creation bonus **Response includes:** - Campaign name and description - Points awarded - Action required (e.g., 'Follow on Instagram') - Icon/image URL - Terms and conditions **Use Cases:** - Display 'Ways to Earn' section in customer portal - Show earning opportunities on product pages - Encourage customer engagement **Authentication:** Customer must be logged in via Shopify customer session # Get customer loyalty points and rewards data Source: https://developers.appstle.com/subscription-storefront-api/loyalty-integration/get-customer-loyalty-points-and-rewards-data /subscription/storefront-api-swagger.json get /subscriptions/cp/api/loyalty-integration/customer Retrieves the loyalty/rewards program data for the currently logged-in customer. This includes points balance, tier status, and available rewards. **Supported Loyalty Programs:** - Appstle Loyalty & Rewards - Yotpo Loyalty & Referrals - Smile.io (via Appstle integration) **What you get:** - Current points balance - Points pending (from recent orders) - VIP tier/level - Points history - Available reward redemptions **Use Cases:** - Display points balance in customer portal - Show available rewards customer can redeem - Display VIP tier badge - Show points expiration dates **Authentication:** Customer must be logged in via Shopify customer session **Note:** Requires loyalty app integration to be configured in merchant settings # Redeem loyalty points for a reward Source: https://developers.appstle.com/subscription-storefront-api/loyalty-integration/redeem-loyalty-points-for-a-reward /subscription/storefront-api-swagger.json post /subscriptions/cp/api/loyalty-integration/redeem Allows a customer to redeem their loyalty points for a specific reward option. This deducts points from their balance and generates a discount code or applies the reward. **What happens:** 1. Validates customer has enough points 2. Deducts points from customer's balance 3. Generates discount code (for discount rewards) 4. Records redemption in customer's history 5. Returns discount code or confirmation **Reward Types:** - **Discount codes**: Generates unique code customer can use at checkout - **Auto-apply discounts**: Automatically applied to next order - **Free products**: Adds free product to next order - **Free shipping**: Waives shipping on next order **Important Notes:** - Points are deducted immediately and cannot be refunded - Discount codes typically expire after 30 days - Some rewards have minimum purchase requirements - Rewards cannot be combined with other discounts (depends on configuration) **Use Cases:** - Customer clicks 'Redeem' button in customer portal - Apply points at checkout - Redeem points for subscription discount **Authentication:** Customer must be logged in via Shopify customer session # Get product swap variant groups for a contract Source: https://developers.appstle.com/subscription-storefront-api/product-catalog/get-product-swap-variant-groups-for-a-contract /subscription/storefront-api-swagger.json post /subscriptions/cp/api/product-swaps-by-variant-groups/{contractId} Retrieves product swap variant groups for the next 10 billing cycles of a specific subscription contract. This endpoint calculates and returns which products will be swapped to in future billing cycles based on configured swap automations. **Response Structure:** Returns a 2D array (`List>`): - **Outer array**: Represents the next 10 billing cycles - **Inner arrays**: Contains the variant(s) that will be swapped to for that specific cycle - **Index 0**: Variants for the next (upcoming) billing cycle - **Index 1**: Variants for the cycle after that - And so on for the next 10 cycles **Variant Details:** Each variant object includes: - **variantId**: Shopify variant ID - **quantity**: Number of units to swap - **title**: Variant title (e.g., "Medium Roast - 12oz") - **image**: Product/variant image URL - **productTitle**: Full product name - **productId**: Shopify product GID - **variantTitle**: Full display name combining product and variant titles - **swapId**: ID of the swap automation rule that triggered this swap **How It Works:** 1. Takes the current products in the subscription 2. Applies configured swap automations for the next 10 cycles 3. Calculates which products will be swapped based on: - Billing cycle number - Swap rule configurations (forBillingCycle, checkForEveryRecurringOrder) - Rule sequence/priority 4. Returns the projected product lineup for each cycle **Use Cases:** - Preview upcoming product swaps in customer portals - Show customers their subscription product timeline - Build interactive swap calendars - Display "what you'll receive" for future orders - Debug and verify swap automation configurations **Important Notes:** - Returns 10 cycles even if no swaps are configured (returns current products) - Multiple variants in an inner array means multiple products will be in that order - Empty inner arrays indicate no products for that cycle (rare edge case) - The swapId can be used to trace which automation rule triggered the swap **Authentication:** Requires valid X-API-Key header # Get variant data for a list of variant IDs Source: https://developers.appstle.com/subscription-storefront-api/product-catalog/get-variant-data-for-a-list-of-variant-ids /subscription/storefront-api-swagger.json get /subscriptions/cp/api/product-infos/variant-data/{variantIds} # List discount rules applicable to bundles Source: https://developers.appstle.com/subscription-storefront-api/product-catalog/list-discount-rules-applicable-to-bundles /subscription/storefront-api-swagger.json get /subscriptions/cp/api/bundle-discount-rules # Proxy a Shopify Storefront GraphQL request from the customer portal Source: https://developers.appstle.com/subscription-storefront-api/product-catalog/proxy-a-shopify-storefront-graphql-request-from-the-customer-portal /subscription/storefront-api-swagger.json post /subscriptions/cp/api/storefront-graphql # Get the first eligible pickup date Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/get-the-first-eligible-pickup-date /subscription/storefront-api-swagger.json get /subscriptions/cp/api/pickup/eligible-dates/first # List available pickup locations Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/list-available-pickup-locations /subscription/storefront-api-swagger.json post /subscriptions/cp/api/pickup/locations # List eligible pickup dates Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/list-eligible-pickup-dates /subscription/storefront-api-swagger.json post /subscriptions/cp/api/pickup/eligible-dates # List eligible pickup times for a date Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/list-eligible-pickup-times-for-a-date /subscription/storefront-api-swagger.json get /subscriptions/cp/api/pickup/eligible-times # List eligible pickup times for a date Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/list-eligible-pickup-times-for-a-date-1 /subscription/storefront-api-swagger.json post /subscriptions/cp/api/pickup/eligible-times # Update pickup and delivery details on a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/store-pickup/update-pickup-and-delivery-details-on-a-subscription-contract /subscription/storefront-api-swagger.json post /subscriptions/cp/api/pickup/subscription-selection # Execute a server-authoritative next-order delay Source: https://developers.appstle.com/subscription-storefront-api/subscription-actions/execute-a-server-authoritative-next-order-delay /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-actions/reschedule-by-days/execute Re-reads the recorded billing attempt, recomputes delayDays 7 or 14 in the shop IANA timezone, re-runs every eligibility guard, and returns a typed outcome. The client never supplies an authoritative target date. # Preview a server-authoritative next-order delay Source: https://developers.appstle.com/subscription-storefront-api/subscription-actions/preview-a-server-authoritative-next-order-delay /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-actions/reschedule-by-days/preview Accepts contractId, optional billingAttemptId, and delayDays 7 or 14. Returns shop-local current and target dates without mutating. reasonCode is one of the SubscriptionActionReasonCode enum values. # Add a fixed pricing bundle line item, with its child products, to a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-fixed-pricing-bundle-line-item-with-its-child-products-to-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/v2/subscription-contracts-add-bundle-line-item # Add a line item to a specific section of a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-line-item-to-a-specific-section-of-a-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/v2/subscription-contracts-add-sectioned-line-item/{contractId} # Add a line item to a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/add-a-line-item-to-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/v2/subscription-contracts-add-line-item # Apply a cancellation-flow discount to a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/apply-a-cancellation-flow-discount-to-a-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-cancellation-discount # Apply a discount code to a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/apply-a-discount-code-to-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-apply-discount # Cancel and delete a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/cancel-and-delete-a-subscription-contract /subscription/storefront-api-swagger.json delete /subscriptions/cp/api/subscription-contracts/{id} # Change contract frequency by selecting a compatible selling-plan frequency Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/change-contract-frequency-by-selecting-a-compatible-selling-plan-frequency /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-frequency-by-compatible-plan # Change contract frequency by selecting a new selling plan Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/change-contract-frequency-by-selecting-a-new-selling-plan /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-frequency-by-selling-plan # Check whether anchor day overwrite is enabled Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/check-whether-anchor-day-overwrite-is-enabled /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts-is-overwrite-anchor-day # Edit custom attributes on a single line item Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/edit-custom-attributes-on-a-single-line-item /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-edit-line-item-attributes # Email customer portal magic link to subscriber Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/email-customer-portal-magic-link-to-subscriber /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts-email-magic-link # Execute a configured cancellation retention offer and record the retention activity Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/execute-a-configured-cancellation-retention-offer-and-record-the-retention-activity /subscription/storefront-api-swagger.json post /subscriptions/cp/api/cancellation-retention-offers/execute # Get current billing cycle for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-current-billing-cycle-for-a-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-details/current-cycle/{contractId} # Get current customer's saved Shopify payment methods Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-current-customers-saved-shopify-payment-methods /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-details/shopify/customer/payment-methods # Get current customer's subscription profile Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-current-customers-subscription-profile /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-customers # Get currently logged-in customer ID Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-currently-logged-in-customer-id /subscription/storefront-api-swagger.json get /subscriptions/cp/api/logged-in-customer Returns the customer ID of the currently authenticated customer from their Shopify session. This endpoint is used by the customer portal UI to identify which customer is logged in. **Use Cases:** - Customer portal initialization - determine who's logged in - Fetch customer-specific data (subscriptions, orders, settings) - Validate customer session before displaying sensitive information - Personalize customer portal UI with customer name/email **How it works:** - Customer must be logged in to their Shopify customer account - Session is validated via Shopify App Proxy - Returns Shopify customer ID (numeric) **Important Notes:** - This endpoint only works when called from the shop domain (via Shopify App Proxy) - Customer must be logged in to Shopify - Returns null if customer is not authenticated - Do not use this for external API integrations - use External APIs with explicit customer ID instead **Authentication:** Requires active Shopify customer session (browser-based) # Get order note for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-order-note-for-a-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-details/customer/{contractId} # Get raw Shopify subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-raw-shopify-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts/contract/{contractId} # Get raw Shopify subscription contract (external view) Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-raw-shopify-subscription-contract-external-view /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts/contract-external/{contractId} # Get raw Shopify subscription contracts for multiple contracts in one call Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-raw-shopify-subscription-contracts-for-multiple-contracts-in-one-call /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts/contract-external-bulk # Get subscription contract as a flattened map Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-subscription-contract-as-a-flattened-map /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-raw/{contractId} # Get valid subscription contracts for a customer Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/get-valid-subscription-contracts-for-a-customer /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-customers-detail/valid/{id} # List a customer's valid subscription contract IDs Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/list-a-customers-valid-subscription-contract-ids /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-customers/valid/{id} # List Shopify fulfillments for a specific order Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/list-shopify-fulfillments-for-a-specific-order /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-details/subscription-fulfillments/order/{orderId} # List Shopify fulfillments for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/list-shopify-fulfillments-for-a-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-details/subscription-fulfillments/{contractId} # Merge an existing contract into another Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/merge-an-existing-contract-into-another /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-contract-details/merge-contract # Read freeze (skip-billing) status of a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/read-freeze-skip-billing-status-of-a-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contracts-freeze-status-detail # Read freeze (skip-billing) status of a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/read-freeze-skip-billing-status-of-a-contract-1 /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-freeze-status-detail # Remove a discount from a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/remove-a-discount-from-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-remove-discount # Remove a line item from a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/remove-a-line-item-from-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-remove-line-item # Replace contract variants in bulk (v2) Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/replace-contract-variants-in-bulk-v2 /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-contract-details/replace-variants-v2 # Reschedule a planned subscription fulfillment Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/reschedule-a-planned-subscription-fulfillment /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contract-details/subscription-fulfillment/reschedule # Set existing Shopify payment method on contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/set-existing-shopify-payment-method-on-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-existing-payment-method # Skip or unskip a product line item for next order Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/skip-or-unskip-a-product-line-item-for-next-order /subscription/storefront-api-swagger.json put /subscriptions/cp/api/skip-unskip-subscription-contracts-product # Swap a product within a Build-a-Box subscription Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/swap-a-product-within-a-build-a-box-subscription /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contract-details/swap-bab-product # Sync shipping price on contract from delivery profile Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/sync-shipping-price-on-contract-from-delivery-profile /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts/sync-shipping-price/{contractId} # Trigger payment method re-collection email Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/trigger-payment-method-re-collection-email /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-payment-method # Update billing interval for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-billing-interval-for-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-billing-interval Updates the billing frequency (how often the customer is charged) for a subscription contract. This comprehensive operation recalculates billing dates, adjusts pricing, updates selling plans, and may also modify delivery intervals. **Key Features:** - Changes billing frequency while maintaining subscription continuity - Automatically adjusts delivery interval when linked to billing - Recalculates next billing date based on store settings - Reprices all line items for the new frequency - Finds and applies matching selling plans - Handles anchor day adjustments for consistent billing - Validates prepaid subscription constraints **Billing Interval Types:** - **DAY**: Daily billing (use with caution) - **WEEK**: Weekly billing (e.g., every 1, 2, 3 weeks) - **MONTH**: Monthly billing (e.g., every 1, 2, 3 months) - **YEAR**: Annual billing **Validation Rules:** - Cannot set the same interval as current (no-op prevention) - For prepaid subscriptions: billing interval must exceed delivery interval - Example: Can't bill monthly if delivering weekly (would be paying for 1 delivery but receiving 4) **Next Billing Date Calculation:** The system uses sophisticated logic to determine the new billing date: 1. Starts from current or last successful billing date 2. Applies store timezone and order time preferences 3. Ensures date is in the future 4. Respects day-of-week preferences if configured 5. May keep current date if 'enableChangeFromNextBillingDate' is false **Side Effects:** - **Delivery Interval**: Updated if currently equal to billing interval - **Line Item Pricing**: Recalculated based on new frequency multiplier - **Selling Plans**: Finds and applies best matching plans for products - **Anchor Days**: Updates billing anchor for consistent scheduling - **Email Notifications**: Sends 'ORDER_FREQUENCY_UPDATED' to customer - **Activity Logs**: Records both billing and delivery changes **Prepaid vs Pay-Per-Delivery:** - Pay-per-delivery: Billing and delivery intervals typically match - Prepaid: Customer pays upfront for multiple deliveries - This endpoint enforces prepaid logic to prevent undercharging **Authentication:** Requires valid X-API-Key header # Update custom attributes on contract line items Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-custom-attributes-on-contract-line-items /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-line-item-attributes # Update delivery method on a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-delivery-method-on-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-delivery-method # Update line item quantities for a classic Build-a-Box subscription Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-line-item-quantities-for-a-classic-build-a-box-subscription /subscription/storefront-api-swagger.json put /subscriptions/cp/api/update-classic-bab-subscription/{contractId} # Update line item quantity on a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-line-item-quantity-on-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-line-item-quantity # Update next billing date for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-next-billing-date-for-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-billing-date Reschedules the next billing date for an active subscription contract. This endpoint allows you to change when the next order will be created and processed. **Key Features:** - Updates the next billing date to the specified date - Optionally reschedules all future orders based on the new date - Syncs the updated date to Shopify - Sends confirmation email to customer **The rescheduleFutureOrder Parameter:** - **true** (default): Updates the next billing date AND recalculates all future queued orders based on the new date. Use this to shift the entire billing schedule. - **false**: Only updates the next billing date. Other queued orders remain unchanged. Use this for one-time date adjustments. **Important Notes:** - The new date must be in the future (with 10 minute grace period) - Date is validated against the shop's timezone - If anchor days are configured, future orders may align to those anchors **Process Flow:** 1. Validates the contract exists and belongs to the shop 2. Validates the new date is not in the past 3. Updates the billing attempt to the new date 4. Syncs the updated nextBillingDate to Shopify 5. If rescheduleFutureOrder=true, regenerates the queue from the new date 6. Sends confirmation email to customer 7. Records activity log entry **Date Format:** - Must be ISO 8601 format with timezone - Examples: `2024-03-15T12:00:00Z`, `2024-03-15T12:00:00+05:30` - URL encode the date when passing as query parameter **Timezone Handling:** - The provided date is used as-is for the billing attempt - Validation (past date check) uses the shop's configured timezone - All dates in the response are in UTC (Z suffix) **Date Restrictions (Customer Portal Only):** When called from customer portal context: - Minimum days from today (skipDaysFromCurrentDate setting) - Maximum days from today (billingDateRestrictToDays setting) - External API calls bypass these restrictions **Authentication:** Requires valid X-API-Key header # Update order note on a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-order-note-on-a-subscription-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-order-note/{contractId} # Update product-level custom attributes on a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-product-level-custom-attributes-on-a-contract /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-product-custom-attributes # Update subscription contract details Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-subscription-contract-details /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contract-details # Update subscription contract order note Source: https://developers.appstle.com/subscription-storefront-api/subscription-contracts/update-subscription-contract-order-note /subscription/storefront-api-swagger.json put /subscriptions/cp/api/update-contract-order-note/{contractId} # Get compatible selling-plan frequencies Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-compatible-selling-plan-frequencies /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/v2/compatible-selling-plan-frequencies Get compatible selling-plan frequency options for a subscription contract # Get delivery options for subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-delivery-options-for-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/contract-delivery-options Retrieves all valid delivery methods available for a specific subscription contract. Returns shipping profiles and delivery options based on the contract's delivery address and product characteristics. **Delivery Information Returned:** - Available shipping profiles - Delivery methods for each profile - Shipping rates and costs - Delivery speed (standard, express, etc.) - Method names and descriptions - Eligibility based on address and products **Filtering Behavior:** By default, returns only delivery methods valid for the contract: - Matches delivery address country/region - Compatible with subscription products - Available for contract weight/dimensions **Include All Methods:** Use header `X-Include-All-Methods: true` to return all delivery methods regardless of eligibility. Useful for admin UIs where you want to show all options. **Use Cases:** - Display delivery method selector in customer portal - Allow customers to change shipping method - Calculate shipping costs for subscription - Validate delivery method during subscription updates - Show upgrade options (standard to express) **Common Scenarios:** **Customer Portal - Change Delivery Speed:** 1. Call this endpoint with contractId 2. Display available methods to customer 3. Customer selects preferred method 4. Update subscription with new delivery method ID **Subscription Creation - Delivery Selection:** 1. Get delivery options for draft contract 2. Present options during checkout/signup 3. Create subscription with selected method **Important Notes:** - Delivery options depend on current contract address - Changing address may change available methods - Costs may vary based on products in subscription - Some methods may have minimum order requirements - International shipping may have additional restrictions **Authentication:** Requires X-API-Key header # Get products data with pagination Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-products-data-with-pagination /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/products Retrieves paginated product catalog data from Shopify, optionally filtered by search term, selling plans, or subscription contracts. This endpoint provides access to your store's product catalog with subscription-specific information. **What This Endpoint Returns:** Product information including: - Product ID, title, handle, and description - Product images and media - Variants with pricing and availability - Associated selling plans (subscription plans) - Product status and tags - Vendor and product type **Pagination:** Uses cursor-based pagination for efficient data retrieval: - `next=true` - Get next page of results - `cursor` - Pagination cursor from previous response - Returns cursor for next page in response **Filtering Options:** 1. **Search** (`search` parameter): - Search by product title, description, or SKU - Partial matching supported - Case-insensitive 2. **Selling Plan Filter** (`sellingPlanIds` parameter): - Filter products by subscription plans - Comma-separated list of selling plan IDs - Only returns products with specified plans 3. **Contract Filter** (`contractId` parameter): - Get products available for specific subscription - Useful for product swap functionality - Returns products compatible with contract **Use Cases:** - Display product catalog in custom subscription UI - Build product selection for subscription creation - Implement product swap functionality - Search products for subscription management - Sync product data to external systems **Authentication:** Requires valid api_key parameter # Get selling plans for products Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-selling-plans-for-products /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/products-selling-plans Retrieves all selling plans (subscription plans) associated with a set of products. This endpoint is useful for determining which subscription options are available for specific products. **Selling Plan Information:** - Selling plan ID and name - Billing and delivery frequencies - Pricing policies (discounts) - Plan description and options - Product associations **Use Cases:** - Display subscription options on product pages - Validate subscription plan availability - Build subscription plan selector UI - Sync plan data across systems **Request:** - Accepts multiple product IDs as comma-separated values - Returns aggregated selling plans from all products **Authentication:** Requires valid api_key parameter # Get single product data by ID Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-single-product-data-by-id /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/product Retrieves detailed information for a specific product by its Shopify product ID. Returns complete product details including all variants, images, and selling plan associations. **Product Information Returned:** - Basic Details: Title, description, handle, status - Variants: All product variants with pricing - Images: Product images and variant-specific images - Selling Plans: Associated subscription plans - Metadata: Tags, vendor, product type - Options: Color, size, and other variant options **Use Cases:** - Display product details in subscription management UI - Validate product availability for subscriptions - Get pricing information for subscription creation - Fetch product data for order processing **Authentication:** Requires valid api_key parameter # Get variant contextual pricing Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/get-variant-contextual-pricing /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/variant-contextual-pricing Retrieves contextual pricing information for a product variant based on currency and country. This endpoint provides localized pricing data for international subscriptions and multi-currency support. **Pricing Information Returned:** - Base price in specified currency - Compare-at price (if applicable) - Country-specific pricing adjustments - Tax information for the region - Currency conversion data **Parameters:** - `variantId` - Shopify variant ID (required) - `currencyCode` - ISO 4217 currency code (e.g., USD, EUR, GBP) (required) - `countryCode` - ISO 3166-1 country code (e.g., US, GB, CA) (optional) **Use Cases:** - Display localized pricing in customer portal - Calculate subscription totals for international customers - Support multi-currency subscriptions - Show accurate pricing based on customer location - Validate pricing before subscription creation **Important Notes:** - Requires Shopify Markets or multi-currency setup - Prices are returned in the requested currency - Country-specific pricing takes precedence over currency-only pricing - Returns 404 if variant doesn't exist or isn't available in specified market **Authentication:** Requires valid api_key parameter # List delivery options available for the shop Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-delivery-options-available-for-the-shop /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/shop-delivery-options # List enabled presentment currencies for the shop Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-enabled-presentment-currencies-for-the-shop /subscription/storefront-api-swagger.json get /subscriptions/cp/api/v2/data/currencies # List live carrier-quoted shipping rates for a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-live-carrier-quoted-shipping-rates-for-a-subscription-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/contract-live-shipping-rates # List product handles for the shop Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-product-handles-for-the-shop /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/product-handles # List products with their selling plans (v2) Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-products-with-their-selling-plans-v2 /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/v2/products-selling-plans # List saved addresses for the current customer Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-saved-addresses-for-the-current-customer /subscription/storefront-api-swagger.json get /subscriptions/cp/api/v2/data/addresses # List subscription products available to the customer Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-subscription-products-available-to-the-customer /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/subscription-products # List subscription products filtered by selling plans Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-subscription-products-filtered-by-selling-plans /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/selling-plan-products # List supported shipping countries Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/list-supported-shipping-countries /subscription/storefront-api-swagger.json get /subscriptions/cp/api/v2/data/countries # Search subscription-enabled products with pagination Source: https://developers.appstle.com/subscription-storefront-api/subscription-data/search-subscription-enabled-products-with-pagination /subscription/storefront-api-swagger.json get /subscriptions/cp/api/data/products1 # Replace product variants in a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-management/replace-product-variants-in-a-subscription-contract /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-contract-details/replace-variants-v3 Replaces existing product variants with new ones in a subscription contract. This endpoint supports both regular subscription products and one-time products, allowing you to swap products, update quantities, and manage the product mix in a subscription. **Key Features:** - **Bulk Replace**: Replace multiple products at once by providing lists of old and new variants - **Line-Specific Replace**: Target a specific line item using oldLineId for precise replacement - **Quantity Management**: Set new quantities for replaced products - **One-Time Products**: Add or remove one-time purchase products independently - **Discount Preservation**: Configurable discount carry-forward logic **Discount Carry Forward Options:** - **PRODUCT_THEN_EXISTING**: First tries to apply the new product's selling plan discount, then falls back to existing discount - **PRODUCT_PLAN**: Always uses the new product's selling plan discount - **EXISTING_PLAN**: Maintains the existing product's discount structure **Important Notes:** - At least one regular subscription product must remain in the contract - One-time products and free products don't count towards the minimum product requirement - The system automatically handles pricing adjustments based on billing/delivery intervals - Shipping prices are automatically recalculated after changes **Use Cases:** - Product upgrades/downgrades (e.g., switching coffee blend or size) - Quantity adjustments during product swap - Adding limited-time or seasonal products as one-time purchases - Bulk product replacements for subscription migrations **Authentication:** Requires valid X-API-Key header # Split or duplicate an existing subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-management/split-or-duplicate-an-existing-subscription-contract /subscription/storefront-api-swagger.json post /subscriptions/cp/api/subscription-contract-details/split-existing-contract Creates a new subscription contract by either splitting (moving) or duplicating selected line items from an existing contract. This endpoint allows you to divide a subscription into multiple contracts or create a copy with specific products. **Split vs Duplicate Mode:** - **Split (isSplitContract=true)**: Moves the selected line items from the original contract to a new contract. The original contract will no longer contain these items. - **Duplicate (isSplitContract=false)**: Creates a new contract with copies of the selected line items while keeping them in the original contract. **Important Notes:** - When splitting, at least one subscription product must remain in the original contract - The new contract inherits all settings from the original: customer, payment method, billing/delivery policies, shipping address, and custom attributes - Billing schedule is automatically generated for the new contract - One-time products and free products don't count towards the minimum product requirement **Use Cases:** - Split a subscription when customer wants different delivery schedules for different products - Create a gift subscription from an existing subscription - Separate products for different shipping addresses - Duplicate a subscription for testing or backup purposes **Authentication:** Requires valid X-API-Key header # Update custom attributes on a subscription contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-management/update-custom-attributes-on-a-subscription-contract /subscription/storefront-api-swagger.json post /subscriptions/cp/api/update-custom-note-attributes Updates or replaces custom key-value attributes on a subscription contract. These attributes are stored with the subscription and can be used to track custom data, preferences, or metadata that's important for your business processes. **Custom Attributes Overview:** Custom attributes are key-value pairs that allow you to store additional information on subscriptions. They are: - Visible in the Shopify admin and accessible via API - Included in order data when subscription orders are created - Preserved across subscription lifecycle events - Useful for integrations and custom workflows **Update Modes:** - **Merge Mode (overwriteExistingAttributes=false)**: Adds new attributes and updates existing ones with matching keys. Other attributes remain unchanged. - **Replace Mode (overwriteExistingAttributes=true)**: Completely replaces all existing attributes with the provided list. **Common Use Cases:** - Store gift messages or special instructions - Track referral sources or marketing campaigns - Add internal reference numbers or tracking codes - Store customer preferences or customization options - Integration data for third-party systems **Important Notes:** - Attribute keys should not conflict with Shopify's reserved attributes - Both keys and values are stored as strings - Changes are logged in the activity history - Invalid discount codes may be automatically removed during update **Authentication:** Requires valid X-API-Key header # Update subscription contract status Source: https://developers.appstle.com/subscription-storefront-api/subscription-management/update-subscription-contract-status /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-status Updates the status of a subscription contract to ACTIVE, PAUSED, or CANCELLED. This endpoint manages the lifecycle of subscriptions with automatic state tracking and notifications. **Status Transitions:** - **ACTIVE**: Resumes a paused subscription, enabling future billing and deliveries - **PAUSED**: Temporarily suspends all billing and deliveries until manually resumed - **CANCELLED**: Permanently terminates the subscription (irreversible) **Key Features:** - Validates status transitions (prevents same-status updates) - Tracks status change timestamps for audit trails - Sends automated email notifications to customers - Creates detailed activity logs for each status change - Handles concurrent modifications with automatic retry - Adjusts next billing date when resuming subscriptions **Permission Requirements (Customer Portal):** When called from customer portal context: - PAUSED status requires 'pauseResumeSub' permission - ACTIVE status (resuming) requires 'resumeSub' permission - CANCELLED status requires 'cancelSub' permission - External API calls bypass these permission checks **Status Change Side Effects:** - **Activating**: Recalculates next billing date, marks activation timestamp - **Pausing**: Stops all scheduled orders, marks pause timestamp - **Cancelling**: Terminates subscription permanently, marks cancellation timestamp **Error Recovery:** The system automatically handles: - Invalid discount codes by removing them and retrying - Concurrent modifications by retrying the operation - This ensures reliable status updates in production environments **Important Notes:** - Status values are case-insensitive - Cancelled subscriptions cannot be reactivated - Paused subscriptions retain all settings and can be resumed - Email notifications are sent automatically unless internally suppressed **Authentication:** Requires valid X-API-Key header # Update subscription delivery address and method Source: https://developers.appstle.com/subscription-storefront-api/subscription-management/update-subscription-delivery-address-and-method /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contracts-update-shipping-address Updates the shipping address or delivery method for a subscription contract. Supports standard shipping, local delivery, and pickup options with comprehensive address validation. **Delivery Method Types:** - **SHIPPING**: Standard shipping to customer address (default) - **LOCAL**: Local delivery within specified zip codes - **PICK_UP**: Customer pickup at designated location **Key Features:** - Zip code restrictions for customer portal access - Optional ShipperHQ address validation - Automatic phone number handling for local delivery - Email notifications to customers - Shipping price recalculation - Activity log tracking with old/new addresses **Zip Code Validation (Customer Portal Only):** When called from customer portal: - Standard shipping: Validates against 'allowToSpecificZipCode' setting - Local delivery: Validates against 'allowToSpecificZipCodeForLocalDelivery' setting - External API calls bypass zip code restrictions **Address Validation:** - If ShipperHQ is configured, addresses are validated for deliverability - Invalid addresses will return appropriate error messages - Helps prevent failed deliveries and shipping issues **Special Handling:** - Local delivery addresses missing phone numbers are auto-populated - Uses customer's phone if available, otherwise defaults to placeholder - Handles missing address components gracefully **Post-Update Actions:** - Sends 'SHIPPING_ADDRESS_UPDATED' email to customer - Creates activity log with address change details - Triggers asynchronous shipping price recalculation - May remove invalid discount codes automatically **Important Notes:** - Province codes should use ISO 3166-2 format (e.g., 'NY' for New York) - Country codes should use ISO 3166-1 alpha-2 format (e.g., 'US') - Phone numbers should include country code for international addresses - Pickup locations must be pre-configured in Shopify **Authentication:** Requires valid X-API-Key header # Delete a one-time add-on from a billing attempt Source: https://developers.appstle.com/subscription-storefront-api/subscription-one-time-products/delete-a-one-time-add-on-from-a-billing-attempt /subscription/storefront-api-swagger.json delete /subscriptions/cp/api/subscription-contract-one-offs-by-contractId-and-billing-attempt-id # Get one-time add-ons for the next upcoming order Source: https://developers.appstle.com/subscription-storefront-api/subscription-one-time-products/get-one-time-add-ons-for-the-next-upcoming-order /subscription/storefront-api-swagger.json get /subscriptions/cp/api/upcoming-subscription-contract-one-offs-by-contractId # List one-time add-on products for a contract Source: https://developers.appstle.com/subscription-storefront-api/subscription-one-time-products/list-one-time-add-on-products-for-a-contract /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-contract-one-offs-by-contractId # Save a one-time add-on for a billing attempt Source: https://developers.appstle.com/subscription-storefront-api/subscription-one-time-products/save-a-one-time-add-on-for-a-billing-attempt /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contract-one-offs-by-contractId-and-billing-attempt-id Discontinued — one-time products are no longer applied to the generated order. This endpoint now always returns 400. # Update quantity of a one-time add-on Source: https://developers.appstle.com/subscription-storefront-api/subscription-one-time-products/update-quantity-of-a-one-time-add-on /subscription/storefront-api-swagger.json put /subscriptions/cp/api/subscription-contract-one-offs-update-quantity # List all selling plans available to the customer Source: https://developers.appstle.com/subscription-storefront-api/subscription-plans/list-all-selling-plans-available-to-the-customer /subscription/storefront-api-swagger.json get /subscriptions/cp/api/subscription-groups/all-selling-plans # Get subscriptionscpapiupcoming order rewards Source: https://developers.appstle.com/subscription-storefront-api/upcoming-order-reward-resource/get-subscriptionscpapiupcoming-order-rewards /subscription/storefront-api-swagger.json get /subscriptions/cp/api/upcoming-order-rewards # Authenticate with the Appstle Subscriptions API Source: https://developers.appstle.com/subscription/authentication Create and manage API keys in the Appstle dashboard, pass the X-API-Key header on every Admin API request, and connect third-party products through the Partner Integration Framework. Every Admin API request must include an API key. You generate keys in your Appstle dashboard — they are never transmitted over the wire in full after creation, so store them securely as soon as you create them. API keys are **server-side only**. Never include them in client-side JavaScript, mobile app source code, public repositories, or any environment where end users could inspect them. ## Creating an API key Log in to your Appstle admin panel and navigate to **Settings → API Key Management**. Click **Create New Key** and give it a descriptive name that identifies the integration it belongs to — for example, `Zapier Integration` or `Mobile App`. The full key value is shown only once. Copy it now and store it in a secure location such as a secrets manager or environment variable store. You cannot retrieve it again after closing this dialog. Set the key as an environment variable in your server environment and read it at runtime. Never hard-code it in source files. Create one key per integration rather than sharing a single key across systems. This lets you revoke access for one integration without disrupting the others. ## Sending the API key Include the key in the `X-API-Key` request header on every Admin API call: ```bash curl theme={null} curl -H "X-API-Key: apst_your-api-key-here" \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/12345" ``` ```javascript Node.js theme={null} const response = await fetch( 'https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/12345', { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); ``` ```python Python theme={null} import os import requests response = requests.get( 'https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/12345', headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) ``` API keys use the `apst_` prefix. Existing legacy keys (created before the prefix was introduced) continue to work without any migration. ### Query parameter alternative You can also pass the key as a query parameter, though header-based authentication is recommended: ``` https://subscription-admin.appstle.com/api/external/v2/...?api_key=apst_your-api-key-here ``` ## Managing keys From **Settings → API Key Management** you can: | Action | How | | ---------- | --------------------------------------------------------------------------------------------------------- | | **Create** | Click "Create New Key". Up to 10 active keys per store. | | **Track** | Each key shows a last-used timestamp so you can identify stale keys. | | **Revoke** | Click the revoke button on any key to instantly disable it. Other keys are unaffected. | | **Rotate** | Create a new key first, update your integration to use it, then revoke the old key. This avoids downtime. | Each store can have up to **10 active API keys**. If you need to create an 11th key, revoke an unused existing key first. ## Partner integrations If you are building a product that connects to Appstle on behalf of multiple merchants — for example a helpdesk, CRM, automation platform, or AI agent — use the [Partner Integration Framework](/subscription/partner-integration). If your application needs to call Appstle's Admin API, use the framework's **Nonce Handshake** connection mode: Appstle provides your Partner ID and Partner Secret during onboarding. Store the secret securely. Call the relevant `/api/partner/{partnerId}/...` endpoints and authenticate with `X-Partner-Secret` or the configured HMAC headers. The merchant approves the request under **Settings → Partner Connections** in Appstle. Appstle delivers a merchant-specific `apst_...` token to your approval callback. Send the scoped token as `X-API-Key`, exactly like a regular API key. Your Partner Secret is only ever used for `/api/partner/...` connection calls — never send it to Admin API endpoints. ```bash theme={null} curl -H "X-API-Key: apst_scoped-partner-token" \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/12345" ``` Scoped partner tokens bypass the paid API plan requirement. Merchants do not need their own Appstle API subscription to use an approved partner integration. ## Storefront API authentication The Storefront API does not use API keys. Instead, it relies on a customer's active Shopify session (the customer must be logged in to the storefront). Requests are routed through Shopify's App Proxy, which handles authentication automatically. See the Storefront API reference in the sidebar for endpoint details. # Build your own subscription frontend Source: https://developers.appstle.com/subscription/build-your-own-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. 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`). ## 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. 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: | 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 %}
{{ group.name }} {% for plan in group.selling_plans %} {% endfor %}
{% endfor %} ``` You can also access this data as JSON for use in JavaScript: ```liquid theme={null} ``` ### 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 ``` 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](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}
``` 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. ```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} ``` 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 | 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}
``` 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 Receive real-time events when subscriptions change. Complete list of widget events you can listen to. Admin API keys and storefront authentication details. Connect a third-party platform on behalf of multiple merchants. Server-side Admin API integration for backend developers. # Appstle Subscriptions frequently asked questions Source: https://developers.appstle.com/subscription/faq Answers to common questions about widget customization, detecting subscription orders, customer portal setup, API authentication, and data privacy. Answers to the questions developers most commonly ask when integrating with Appstle Subscriptions. ## Widget & storefront No. You do not need any Appstle API to display subscription options on product pages or add subscriptions to cart. All selling plan data is already available in Shopify's native product JSON. When you create subscription plans in the Appstle admin, Shopify automatically includes them in the product data on every product page. **Read selling plans from the product JSON endpoint:** ```javascript theme={null} fetch('/products/your-product-handle.js') .then(response => response.json()) .then(product => { // All selling plans are here console.log(product.selling_plan_groups); }); ``` **Read selling plans in Liquid:** ```liquid theme={null} {% for selling_plan_group in product.selling_plan_groups %}

{{ selling_plan_group.name }}

{% for selling_plan in selling_plan_group.selling_plans %} {% endfor %} {% endfor %} ``` **Add a subscription to cart:** ```javascript theme={null} fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: variantId, quantity: 1, selling_plan: sellingPlanId // From product.selling_plan_groups }) }); ``` The Appstle APIs are useful when you need to manage existing subscriptions — not for displaying or creating them at checkout.
Yes. There are four approaches: 1. **CSS overrides** — The widget uses classes prefixed with `appstle_` that you can target in your theme CSS. 2. **Merchant portal settings** — Configure widget text, labels, and layout options directly in the Appstle admin without writing code. 3. **JavaScript hooks** — Listen to [widget events](/subscription/javascript-hooks) to add custom behavior when the widget loads, when the customer selects a plan, etc. 4. **Build your own widget** — Use Shopify's native `selling_plan_groups` product data to build a completely custom widget. The `appstle-subscription.js` script is automatically injected into your storefront when the app is installed. It: 1. Reads the product's `selling_plan_groups` from Shopify's native product JSON 2. Renders the subscription widget based on your Appstle admin configuration 3. Handles selling plan selection and cart integration 4. Dispatches [JavaScript events](/subscription/javascript-hooks) you can hook into No API calls to Appstle servers are made to display the widget — everything comes from Shopify's product data.
## Subscriptions & orders 1. The customer selects a subscription option and adds the product to cart 2. At checkout, Shopify creates a **subscription contract** linked to the selling plan 3. Appstle manages the recurring billing cycle, sending billing attempts to Shopify at the configured frequency 4. Shopify processes the payment and creates new orders automatically on each cycle You can determine this directly from Shopify's Order API — no Appstle API call needed. Check the `sellingPlanAllocation` field on each line item. **GraphQL query:** ```graphql theme={null} { order(id: "gid://shopify/Order/ORDER_ID") { name lineItems(first: 50) { edges { node { title quantity sellingPlanAllocation { sellingPlan { id name } } } } } } } ``` If `sellingPlanAllocation` is non-null, that line item is a subscription purchase. If it is null, it is a one-time purchase. **Subscription line item:** ```json theme={null} { "title": "Premium Coffee Blend", "quantity": 1, "sellingPlanAllocation": { "sellingPlan": { "id": "gid://shopify/SellingPlan/123456789", "name": "Deliver every 30 days" } } } ``` **One-time line item:** ```json theme={null} { "title": "Coffee Mug", "quantity": 1, "sellingPlanAllocation": null } ``` An order can contain both subscription and one-time line items. Always check each line item individually. If you use the Shopify REST Admin API, the equivalent field is `selling_plan_allocation` on each line item in the Order resource. Yes. Appstle provides a built-in customer portal that merchants can embed on their store's account page. Customers can: * View active subscriptions * Skip upcoming orders * Pause or resume subscriptions * Swap products or variants * Update shipping address * Change payment method * Cancel subscriptions (subject to the merchant's configured rules) The portal is configured in the Appstle admin and powered by the Storefront API. You can also build a fully custom portal using the Storefront API directly. ## API & integration Pass your API key in the `X-API-Key` header on every request: ```bash theme={null} curl -H "X-API-Key: apst_your-api-key" \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/12345" ``` Generate API keys in the Appstle admin under **Settings → API Key Management**. You can create up to 10 keys per store. See the [Authentication](/subscription/authentication) page for full details. Yes. Appstle supports real-time webhooks for all major subscription events: * Subscription created, updated, activated, paused, cancelled * Billing success, failure, skipped * Upcoming order notifications * Billing interval and next order date changes Webhooks are powered by Svix and include signature verification, automatic retries, and delivery logs. Configure them in **Settings → Webhooks**. See the [Webhooks](/subscription/webhooks) page for full setup instructions. Use the [Partner Integration Framework](/subscription/partner-integration) if your product connects to Appstle on behalf of multiple merchants — for example a helpdesk, CRM, AI agent, or automation platform. Appstle onboards your app once and issues a Partner ID and Partner Secret for the connection handshake. Each time a merchant approves a connection, Appstle issues a scoped `apst_...` token for that store — send it as `X-API-Key`, exactly like a regular API key. If you are building an integration for a single store you own, you only need a regular API key. See [Authentication](/subscription/authentication) for details. No. The Storefront API runs exclusively through Shopify's App Proxy and requires a logged-in browser session on your storefront domain. It cannot be called from mobile apps, backend servers, or any environment outside the storefront. For mobile apps, use the Admin API with an `X-API-Key` header instead. ## Data & privacy All data is hosted on AWS (US-West-1, California) infrastructure with industry-standard security: * TLS 1.2+ encryption in transit * AES-256 encryption at rest (AWS KMS) * VPC isolation and private subnets * SOC, ISO, and PCI-DSS certified infrastructure All merchant and customer data associated with your store is deleted from Appstle's systems upon app uninstallation. Deletion is triggered automatically via Shopify's `app/uninstalled` webhook. ## Still have questions? Contact [support@appstle.com](mailto:support@appstle.com) or explore the rest of the documentation: Full walkthrough for building backend integrations. Real-time event notifications setup and reference. Storefront widget events for custom UI and analytics. # Appstle Subscriptions integration guide Source: https://developers.appstle.com/subscription/integration-guide Integrate with Appstle Subscriptions: authentication, base URL, subscription management, product operations, discounts, shipping, past orders, and partner connections. This guide covers everything you need to build a full integration with Appstle Subscriptions. It assumes you already have an API key — if not, see the [Authentication](/subscription/authentication) page first. ## Base URL All Admin API endpoints share this base URL: ``` https://subscription-admin.appstle.com/api/external/v2/ ``` ## Authentication Pass your API key in the `X-API-Key` header on every request: ```bash theme={null} curl -H "X-API-Key: apst_your-api-key-here" \ "https://subscription-admin.appstle.com/api/external/v2/..." ``` For partner integrations that connect on behalf of multiple merchants, use the [Partner Integration Framework](/subscription/partner-integration). After a merchant approves a **Nonce Handshake** connection, Appstle issues your application a scoped API token for that merchant. Send that token as `X-API-Key`: ```bash theme={null} curl -H "X-API-Key: apst_scoped-partner-token" \ "https://subscription-admin.appstle.com/api/external/v2/..." ``` See [Authentication](/subscription/authentication) for merchant API key management and [Partner integration](/subscription/partner-integration) for the complete connection flow. ## Looking up customer subscriptions ### Get all subscriptions for a customer ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/{customerId}" \ -H "X-API-Key: YOUR_API_KEY" ``` The response includes active, paused, and cancelled subscriptions along with products, next billing date, shipping address, and delivery method. ### Check if a customer has active subscriptions Returns an array of subscription contract IDs. An empty array means no active subscriptions. ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/{customerId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Get full contract details ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details?contractId={contractId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Get upcoming orders ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-billing-attempts/top-orders?contractId={contractId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Subscription management ### Cancel a subscription ```bash theme={null} curl -X DELETE \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts/{contractId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ### Pause a subscription ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-status" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "status": "PAUSED"}' ``` ### Resume a subscription ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-status" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "status": "ACTIVE"}' ``` Valid `status` values are `ACTIVE`, `PAUSED`, and `CANCELLED`. ### Reschedule the next billing date ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-billing-date" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "nextBillingDate": "2026-03-15T00:00:00Z"}' ``` ### Update billing frequency ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-billing-interval" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "billingIntervalCount": 2, "billingInterval": "MONTH"}' ``` Valid `billingInterval` values: `DAY`, `WEEK`, `MONTH`, `YEAR`. ### Skip an upcoming order ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-billing-attempts/skip-order/{billingAttemptId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Product management ### Add a product to a subscription ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-add-line-item" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "variantId": "{variantId}", "quantity": 1}' ``` ### Remove a product from a subscription ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-remove-line-item" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "lineId": "{lineId}"}' ``` ### Update product quantity ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-line-item-quantity" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "lineId": "{lineId}", "quantity": 3}' ``` ## Discounts ### Apply a discount code ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-apply-discount" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}", "discountCode": "SAVE10"}' ``` ### Remove a discount ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-remove-discount" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contractId": "{contractId}"}' ``` ## Shipping ### Update shipping address ```bash theme={null} curl -X PUT \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contracts-update-shipping-address" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contractId": "{contractId}", "address1": "123 Main St", "city": "San Francisco", "province": "California", "country": "United States", "zip": "94105" }' ``` ## Past orders ### Get order history ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-billing-attempts/past-orders?contractId={contractId}" \ -H "X-API-Key: YOUR_API_KEY" ``` ## Rate limits Requests are rate-limited per store. If you receive a `429 Too Many Requests` response, implement exponential backoff before retrying. Do not immediately retry at full speed. ## Partner integrations If you are building a platform that connects to Appstle on behalf of multiple merchants, use the [Partner Integration Framework](/subscription/partner-integration). It provides: * A standard connection and merchant-approval flow * One isolated, revocable API token per connected merchant * External API access without requiring each merchant to purchase API access * Automatic token revocation when a merchant disconnects Your Partner Secret authenticates `/api/partner/...` connection calls through `X-Partner-Secret`. After an approved **Nonce Handshake**, use the issued merchant-scoped token as `X-API-Key` when calling `/api/external/v2/...`. To get onboarded, email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email. The [Partner integration guide](/subscription/partner-integration) lists the complete onboarding requirements and implementation steps. ## Further reading Receive real-time events when subscriptions change. No-code automation using Shopify's built-in workflow engine. Create, rotate, and revoke merchant API keys. Connect your platform and receive scoped API tokens for merchants. # Appstle Subscriptions: Recurring Revenue for Shopify Source: https://developers.appstle.com/subscription/introduction Learn how Appstle Subscriptions works, when to use the Admin API vs. the Storefront API, and what you can build with each interface. Appstle Subscriptions gives Shopify merchants a complete platform for selling and managing recurring products. Once you install the app, Shopify stores subscription contracts natively — and Appstle exposes two REST API surfaces so you can build integrations, automation workflows, custom portals, and more on top of that data. ## Available APIs Appstle Subscriptions exposes two REST API surfaces. Pick the one that matches where your code runs: * **Admin API** — server-side, authenticated with `X-API-Key`. For backend integrations, mobile apps, automation, and admin dashboards. Browse the full reference under **Admin API** in the sidebar. * **Storefront API** — customer-facing, accessed through Shopify App Proxy. For custom subscription portals on your storefront. Browse the full reference under **Storefront API** in the sidebar. ## Which API should you use? * You are building a **server-side integration** (backend service, CRM connector, helpdesk plugin) * You are building a **mobile app** (iOS or Android) * You need to **manage subscriptions programmatically** on behalf of merchants * You want to run **bulk operations** or scheduled automation * You are building an **admin dashboard** or reporting tool * Your code runs **anywhere outside a customer's browser** on the storefront Admin API requests require an `X-API-Key` header. Keys are created in the Appstle dashboard under **Settings → API Key Management**. Never expose keys in client-side code. * You are building a **custom customer portal** hosted on your Shopify storefront * You want customers to **self-manage their subscriptions** (skip, pause, swap products, update payment) * Your code runs **inside the storefront** with a logged-in customer session * You are customizing the subscription portal **theme or appearance** Storefront APIs run exclusively through Shopify's App Proxy and require the customer to be logged in. They will not accept API key authentication and cannot be called from a backend server. ## Key features Create, update, pause, resume, and cancel subscription contracts. Full lifecycle control from initial checkout through cancellation. Process payments, handle billing failures, update payment methods, and manage billing cycles and retry logic. Configure subscription-eligible products, manage selling plans, handle product swaps, and set up Build-a-Box bundles. Manage subscription order cycles, skip upcoming orders, reschedule billing dates, and control delivery schedules. Access revenue metrics, billing history, order counts per subscription contract, and detailed past-order reports. Integrate loyalty programs that let customers earn and redeem points on their subscription orders. Apply custom CSS, configure label translations, and manage theme settings for the storefront widget and customer portal. Connect with Shopify Flow, listen to JavaScript storefront events, and receive real-time webhook notifications. ## Base URL All Admin API endpoints use: ``` https://subscription-admin.appstle.com/api/external/v2/ ``` Storefront API endpoints are accessed through your store's Shopify App Proxy — no separate base URL is needed. ## HTTP status codes All API responses use standard HTTP status codes. | Code | Meaning | | ----- | ------------------------------------------ | | `200` | Success | | `201` | Resource created | | `400` | Bad request — invalid parameters | | `401` | Unauthorized — missing or invalid API key | | `403` | Forbidden — key lacks required permissions | | `404` | Not found | | `429` | Rate limit exceeded | | `500` | Server error | Error responses follow this shape: ```json theme={null} { "error": "Unauthorized", "message": "Invalid API key provided", "status": 401 } ``` ## Next steps Create API keys and learn how to authenticate every request. Make your first API call in under five minutes. End-to-end walkthrough covering the most common integration workflows. Receive real-time event notifications when subscriptions change. # JavaScript events for the Appstle storefront widget Source: https://developers.appstle.com/subscription/javascript-hooks Listen to DOM events fired by the Appstle subscription widget, cart, customer portal, and Build-a-Box to add analytics, custom UI, and storefront integrations. Appstle Subscriptions dispatches JavaScript events from `appstle-subscription.js`, which is automatically injected into your storefront when the app is installed. You can listen to these events to track subscription interactions in analytics tools, show or hide custom UI, and react to customer actions in the cart and portal. All events are dispatched on both `document` and `window`, so you can attach listeners to either. ```javascript theme={null} document.addEventListener('AppstleSubscription:SubscriptionWidget:SellingPlanSelected', function(event) { console.log('Selling plan selected, widget ID:', event.detail); }); ``` These events are dispatched by `appstle-subscription.js`, which loads automatically on your storefront once the Appstle Subscriptions app is installed. No additional configuration is required to start listening. ## Subscription widget events These events fire when customers interact with the subscription widget on product pages. | Event name | Fires when | `event.detail` | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `AppstleSubscription:SubscriptionWidget:widgetInitialised` | Widget loads on a product page | Widget ID | | `AppstleSubscription:SubscriptionWidget:SubscriptionWidgetUpdated` | Widget is re-rendered (e.g. on variant change) | Widget ID | | `AppstleSubscription:SubscriptionWidget:SellingPlanSelected` | Customer selects a subscription option | Widget ID | | `AppstleSubscription:SubscriptionWidget:SellingPlanRemoved` | Subscription selling plan is removed from cart | Widget ID | | `AppstleSubscription:SubscriptionWidget:SellingPlanDeSelected` | Customer switches away from subscription | Widget ID | | `AppstleSubscription:SubscriptionWidget:sellingPlanChanged` | Customer changes the selling plan (frequency, etc.) | Selling Plan ID | | `AppstleSubscription:SubscriptionWidget:AddToCartIntent` | Customer initiates an add-to-cart with a subscription plan selected (fires before the cart request) | `{ trigger: 'form_submit' \| 'fetch_form_data' \| 'appstle_add_to_cart', widgetId?: string }` | | `appstle_widget_updated` | Widget DOM is updated (jQuery event on `document`) | — | ## Cart widget events | Event name | Fires when | `event.detail` | | --------------------------------------------------------------- | ----------------------------------------------------------- | -------------- | | `AppstleSubscription:CartWidget:Updated` | Cart widget is updated | — | | `AppstleSubscription:SubscriptionWidget:SwitchedToOneTime` | Customer switches a cart item from subscription to one-time | Line item key | | `AppstleSubscription:SubscriptionWidget:SwitchedToSubscription` | Customer switches a cart item from one-time to subscription | Line item key | ## Customer portal events | Event name | Fires when | `event.detail` | | ------------------------------------------------- | --------------------------------------------- | -------------- | | `AppstleSubscription:CustomerPortal:ReadyToEmbed` | Customer portal is ready to be embedded | — | | `AppstleSubscription:CustomerPortal:Embedded` | Customer portal has been embedded in the page | — | ## Build-a-Box events | Event name | Fires when | `event.detail` | | --------------------------------------------- | ---------------------------------------- | -------------- | | `AppstleSubscription:Bab:Embedded` | Build-a-Box widget has been embedded | — | | `appstleBundlesAppliedVolumeDiscount:request` | Volume discount is requested for bundles | `true` | ## Other events | Event name | Fires when | `event.detail` | | ---------------------- | ------------------------------------------------ | -------------- | | `OpenAppstleContracts` | Request to open the contracts/subscriptions view | — | ## Code examples ### Track subscription selections in analytics ```javascript theme={null} document.addEventListener('AppstleSubscription:SubscriptionWidget:SellingPlanSelected', function(event) { // Google Analytics 4 gtag('event', 'subscription_selected', { widget_id: event.detail }); }); ``` ### Show custom UI when the widget loads ```javascript theme={null} document.addEventListener('AppstleSubscription:SubscriptionWidget:widgetInitialised', function(event) { const banner = document.querySelector('.my-custom-subscription-banner'); if (banner) { banner.style.display = 'block'; } }); ``` ### React to cart subscription changes ```javascript theme={null} document.addEventListener('AppstleSubscription:SubscriptionWidget:SwitchedToSubscription', function(event) { console.log('Item switched to subscription, line item key:', event.detail); // Update custom cart UI, apply promotions, etc. }); document.addEventListener('AppstleSubscription:SubscriptionWidget:SwitchedToOneTime', function(event) { console.log('Item switched to one-time, line item key:', event.detail); }); ``` ### Run custom logic around customer portal embedding ```javascript theme={null} document.addEventListener('AppstleSubscription:CustomerPortal:ReadyToEmbed', function() { // Run setup logic before the portal renders console.log('Customer portal is ready to embed'); }); document.addEventListener('AppstleSubscription:CustomerPortal:Embedded', function() { // Run post-render logic, e.g. hide a loading spinner const loader = document.querySelector('.portal-loader'); if (loader) loader.remove(); }); ``` ### Track selling plan changes ```javascript theme={null} document.addEventListener('AppstleSubscription:SubscriptionWidget:sellingPlanChanged', function(event) { const newSellingPlanId = event.detail; console.log('Customer changed to selling plan:', newSellingPlanId); // Update UI to reflect new frequency/discount }); ``` # Shopify metafields and tags for Appstle Subscriptions Source: https://developers.appstle.com/subscription/metafields-and-tags Reference for all Appstle Subscriptions metafields (namespace, keys, types) and customer/order tags, including Liquid template support for dynamic tag values. Appstle Subscriptions uses Shopify metafields and tags to store subscription data, power the storefront widget, and enable automation workflows. All metafields use the namespace `appstle_subscription` without an `$app:` prefix, which means they are publicly readable by other apps, themes, and Liquid templates. ## Metafields overview Metafields are set on four Shopify resource types: | Resource | Keys | Visibility | Purpose | | ------------ | ---- | ---------- | --------------------------------------------------------- | | Shop | 15+ | Public | Widget settings, selling plans, Build-a-Box configuration | | Selling Plan | 1 | Public | Individual selling plan metadata | | Order | 1 | Public | Subscription context for each order | | Customer | 1 | Public | All subscription contracts for the customer | ## Shop metafields Shop metafields store the app's configuration and are written whenever a merchant saves settings in the Appstle admin. Updates are synchronous (immediate). ### `appstle_subscription` / `setting` | Property | Value | | ------------ | --------------------------------------------------------------------------------------------------------- | | Type | `json` | | Purpose | Core widget and app configuration — UI settings, asset paths, selling plans, labels, and validation rules | | Updated when | Merchant saves settings | ```json theme={null} { "widgetEnabled": true, "sellingPlans": [], "assetPaths": { "js": "https://cdn.appstle.com/...", "css": "https://cdn.appstle.com/..." }, "labels": {}, "validationRules": {} } ``` ### `appstle_subscription` / `labels` | Property | Value | | ------------ | ---------------------------------------------------------------- | | Type | `multi_line_text_field` | | Purpose | Localized UI labels and translations for the subscription widget | | Updated when | Merchant saves label translations | ### `appstle_subscription` / `shop_info` | Property | Value | | -------- | ------------------------------------------------------------------- | | Type | `json` | | Purpose | Shop metadata including money format, feature flags, and API tokens | ### `appstle_subscription` / `selling_plans` | Property | Value | | -------- | ------------------------------- | | Type | `json` | | Purpose | Free-product selling plans only | ### `appstle_subscription` / `all_Selling_Plans` | Property | Value | | -------- | -------------------------------------- | | Type | `json` | | Purpose | All selling plans including paid plans | The key uses mixed case (`all_Selling_Plans`) — this is intentional and must not be changed. ### `appstle_subscription` / `checkout_validation` | Property | Value | | -------- | ----------------------------------------------------------------------------------------------- | | Type | `json` | | Purpose | Checkout validation rules including duplicate subscription restrictions and per-customer limits | ```json theme={null} { "preventDuplicateSubscriptions": true, "maxSubscriptionsPerCustomer": 5, "rules": [] } ``` ### Widget templates | Key | Type | Purpose | | -------------------------- | ----------------------- | --------------------------------------------------------------------------- | | `widget_template_html` | `multi_line_text_field` | Custom widget template HTML (only set when a custom template is configured) | | `all_widget_template_html` | `json` | Map of all available widget templates | ### Build-a-Box metafields | Key | Type | Purpose | | ----------------------------- | --------- | ---------------------------------------------------------------- | | `bundle` | `json` | Bundle/Build-a-Box configuration | | `bab_subscription_css` | `json` | Build-a-Box subscription CSS styles | | `bab_customization_css` | `json` | Build-a-Box customization CSS | | `bab_validation_info` | `json` | Build-a-Box validation rules | | `bab_setting_info` | `json` | Build-a-Box settings | | `bab_info_0`, `bab_info_1`, … | `json` | Individual bundle details, one per enabled bundle (zero-indexed) | | `total_bab` | `integer` | Total count of enabled Build-a-Box bundles | The `bab_info_*` keys are zero-indexed. A store with three bundles has keys `bab_info_0`, `bab_info_1`, and `bab_info_2`. ## Selling plan metafields ### `appstle_subscription` / `selling_plan` | Property | Value | | ------------ | ----------------------------------------------------------------------- | | Type | `json` | | Resource | Selling Plan | | Purpose | Individual selling plan metadata — frequency, billing policy, discounts | | Updated when | Selling plan is created or modified | ```json theme={null} { "frequencyCount": 1, "frequencyInterval": "MONTH", "billingPolicy": { "interval": "MONTH", "intervalCount": 1 }, "discountType": "PERCENTAGE", "discountValue": 10.0 } ``` ## Order metafields ### `appstle_subscription` / `details` | Property | Value | | ------------- | -------------------------------------------------------------------- | | Type | `json` | | Resource | Order | | Purpose | Full subscription contract context at the time the order was created | | Updated when | Order is created via subscription (initial or recurring billing) | | Update timing | Asynchronous — typically a few seconds after the order is created | ```json theme={null} { "customer": { "id": "gid://shopify/Customer/1234567890" }, "subscriptionContract": { "id": "gid://shopify/SubscriptionContract/9876543210", "status": "ACTIVE", "sellingPlanIds": ["gid://shopify/SellingPlan/111"], "sellingPlanNames": ["Monthly Subscription - 10% off"], "variantIds": ["gid://shopify/ProductVariant/222"], "variantNames": ["Default Title"], "currentCycle": 3, "groupPlanNames": ["Subscribe & Save"], "cancellationReason": null }, "lineItems": [ { "variantId": "gid://shopify/ProductVariant/222", "title": "Premium Coffee Beans", "productId": "gid://shopify/Product/333", "sellingPlanId": "gid://shopify/SellingPlan/111", "sellingPlanName": "Monthly Subscription - 10% off", "sku": "COFFEE-PREMIUM-1KG" } ], "firstOrder": { "id": "gid://shopify/Order/444", "createdAt": "2025-01-15T10:30:00Z" } } ``` ## Customer metafields ### `appstle_subscription` / `subscriptions` | Property | Value | | ------------- | --------------------------------------------------------------------------- | | Type | `json` | | Resource | Customer | | Purpose | All subscription contracts for this customer with full details | | Updated when | Any contract changes — created, updated, paused, cancelled, billing attempt | | Update timing | Asynchronous — typically a few seconds after any contract change | ```json theme={null} [ { "id": "gid://shopify/SubscriptionContract/9876543210", "status": "ACTIVE", "sellingPlanNames": ["Monthly Subscription - 10% off"], "nextBillingDate": "2025-04-15T10:30:00Z", "lineItems": [ { "title": "Premium Coffee Beans", "variantId": "gid://shopify/ProductVariant/222", "sku": "COFFEE-PREMIUM-1KG" } ] } ] ``` Customer metafield updates are **asynchronous**. There may be a delay of a few seconds between a contract change and the metafield reflecting that change. Do not rely on this metafield for real-time data in time-sensitive operations. ## Using metafields in Liquid All metafields use the `appstle_subscription` namespace without an `$app:` prefix, so they are accessible directly in Liquid: ```liquid theme={null} {{ shop.metafields.appstle_subscription.setting }} {{ customer.metafields.appstle_subscription.subscriptions }} ``` ## Order tags Order tags are static strings applied to identify the type of subscription order. They are never removed once applied. | Tag | Config field | Default value | Applied when | | ---------------- | ------------------- | -------------------------------------- | ---------------------------------------- | | First-time order | `firstTimeOrderTag` | `appstle_subscription_first_order` | Initial subscription order is created | | Recurring order | `recurringOrderTag` | `appstle_subscription_recurring_order` | Each subsequent billing order is created | Configure tags in **Appstle Admin → Settings → Order Tags**. If you need to apply tags to orders created before you configured tags, use the `applyMissedOrderTags` API endpoint to backfill them. ## Customer tags Customer tags are dynamic — they change as subscription status changes. They follow a strict priority hierarchy: **Active > Paused > Inactive**. Only one status tag is active at a time. | Status | Condition | Config field | Default value | | -------- | ------------------------------------------------ | --------------------------------- | ---------------------------------------- | | Active | Customer has 1+ active contracts | `customerActiveSubscriptionTag` | `appstle_subscription_active_customer` | | Paused | Customer has paused contracts but no active ones | `customerPausedSubscriptionTag` | `appstle_subscription_paused_customer` | | Inactive | All contracts cancelled | `customerInActiveSubscriptionTag` | `appstle_subscription_inactive_customer` | Tags are updated on every subscription lifecycle event: contract created, paused, resumed, cancelled, and billing attempts. ### Liquid template variables Customer tags support Liquid template syntax. Wrap variables in double curly braces: ``` active_subscriber_{{contract.sellingPlanNames}} ``` **Available variables:** | Variable | Type | Description | Example | | --------------------------------- | ------ | ------------------------------------ | ----------------------------------------- | | `{{customer.id}}` | String | Shopify customer GID | `gid://shopify/Customer/1234567890` | | `{{contract.id}}` | String | Subscription contract GID | `gid://shopify/SubscriptionContract/9876` | | `{{contract.sellingPlanIds}}` | String | Comma-separated selling plan IDs | `gid://shopify/SellingPlan/111` | | `{{contract.sellingPlanNames}}` | String | Comma-separated selling plan names | `Monthly Subscription - 10% off` | | `{{contract.variantIds}}` | String | Comma-separated variant IDs | `gid://shopify/ProductVariant/222` | | `{{contract.variantNames}}` | String | Comma-separated variant names | `Default Title` | | `{{contract.currentCycle}}` | Number | Current billing cycle number | `3` | | `{{contract.cancellationReason}}` | String | Cancellation reason if cancelled | `Too expensive` | | `{{order.id}}` | String | First order GID | `gid://shopify/Order/444` | | `{{order.createdAt}}` | String | First order creation date (ISO 8601) | `2025-01-15T10:30:00Z` | **Template examples:** ``` # Static tag (default behavior) appstle_subscription_active_customer # Dynamic tag with selling plan name active_subscriber_{{contract.sellingPlanNames}} # Result: active_subscriber_Monthly Subscription - 10% off # Dynamic tag with cycle count subscriber_cycle_{{contract.currentCycle}} # Result: subscriber_cycle_3 # Dynamic tag with variant name subscribed_to_{{contract.variantNames}} # Result: subscribed_to_Default Title ``` ## Complete metafield reference | Resource | Namespace | Key | Type | | ------------ | ---------------------- | -------------------------- | ------------------------ | | Shop | `appstle_subscription` | `setting` | json | | Shop | `appstle_subscription` | `widget_template_html` | multi\_line\_text\_field | | Shop | `appstle_subscription` | `all_widget_template_html` | json | | Shop | `appstle_subscription` | `bundle` | json | | Shop | `appstle_subscription` | `labels` | multi\_line\_text\_field | | Shop | `appstle_subscription` | `selling_plans` | json | | Shop | `appstle_subscription` | `checkout_validation` | json | | Shop | `appstle_subscription` | `bab_subscription_css` | json | | Shop | `appstle_subscription` | `bab_customization_css` | json | | Shop | `appstle_subscription` | `shop_info` | json | | Shop | `appstle_subscription` | `bab_validation_info` | json | | Shop | `appstle_subscription` | `bab_setting_info` | json | | Shop | `appstle_subscription` | `all_Selling_Plans` | json | | Shop | `appstle_subscription` | `bab_info_[0-N]` | json | | Shop | `appstle_subscription` | `total_bab` | integer | | Selling Plan | `appstle_subscription` | `selling_plan` | json | | Order | `appstle_subscription` | `details` | json | | Customer | `appstle_subscription` | `subscriptions` | json | ## FAQ Yes. All subscription metafields use the `appstle_subscription` namespace without an `$app:` prefix, so they are accessible in Liquid via `{{ shop.metafields.appstle_subscription.setting }}`, `{{ customer.metafields.appstle_subscription.subscriptions }}`, and so on. Customer metafield updates are processed asynchronously. Typical latency is a few seconds, but during high-traffic periods it may take a bit longer. Tags follow the priority hierarchy Active > Paused > Inactive. If a customer has both active and paused subscriptions, the active tag takes precedence. The inactive tag is only applied when all contracts are cancelled. Yes. Use the `applyMissedOrderTags` API endpoint to apply tags to orders that were created before your tag configuration was set up. # Partner Integration Framework overview Source: https://developers.appstle.com/subscription/partner-framework-overview How the Appstle Subscriptions Partner Integration Framework works — one handshake per merchant, a scoped API token, no API plan required, automatic revocation. The Partner Integration Framework lets your product — a helpdesk, CRM, email platform, review app, or AI agent — connect to Appstle Subscriptions on behalf of many merchants. Instead of asking each merchant to create and paste an API key, your app completes a one-time handshake per store and receives a **scoped API token** for it. ## How a connection works You receive a **Partner ID** and **Partner Secret** used only for connection calls. Either from your product's UI, or from **Settings → Partner Connections** in their Appstle dashboard. Connections your app initiates stay pending until the merchant approves them in Appstle. Pending requests expire after 30 days. Appstle delivers a merchant-specific `apst_...` token to your callback. Send it as `X-API-Key` on Admin API calls — exactly like a regular API key. When a merchant disconnects your app — or uninstalls Appstle — the token is revoked immediately. ## Why use it * **No API plan required** — merchants are never billed for partner API usage * **One isolated token per merchant** — no shared credentials, no manual key exchange, individually revocable * **Merchant-controlled** — merchants see, approve, and disconnect partners from their own dashboard * **Automatic cleanup** — access is revoked the moment a merchant disconnects or uninstalls ## Access levels Your app's permission level is set during onboarding: | Permission | What your app can do | | ---------------- | ------------------------------------------------------------------------------------ | | **Read Only** | View subscription contracts, billing schedules, and order history | | **Read & Write** | Everything above, plus subscription actions such as pausing, skipping, or cancelling | ## Connection modes | Mode | Use it when | Your app receives | | ----------------------------- | ------------------------------------------ | --------------------------------------------------- | | **Nonce Handshake** (default) | Your app needs to call Appstle's Admin API | A merchant-scoped `apst_...` API token | | **Simple Token Exchange** | Appstle should push data to *your* API | No Appstle token — Appstle stores a token you issue | ## Get started Email [support@appstle.com](mailto:support@appstle.com) with your company name, product description, base URL, and contact email to get onboarded. Then follow the [Partner integration guide](/subscription/partner-integration) for the full implementation — endpoints, handshake, callbacks, and testing. # Partner Integration Framework Source: https://developers.appstle.com/subscription/partner-integration Build a zero-configuration integration between your app and Appstle Subscriptions, with scoped API tokens per merchant. Build a seamless, zero-configuration integration between your app and Appstle Subscriptions. Once connected, your app gets a scoped API token for each merchant — no manual key exchange needed. **Why become a Partner?** * **Frictionless merchant onboarding** — one-click connect from either dashboard * **No API paywall** — merchants don't need a paid API plan to use your integration * **Scoped tokens** — each merchant gets an isolated API key; revocable at any time * **Automatic cleanup** — when a merchant disconnects or uninstalls, access is revoked instantly ## How it works The Partner Integration Framework uses a secure handshake protocol. Either side — your app or Appstle — can initiate the connection. Both flows end with your app receiving a scoped API token. ```mermaid theme={null} sequenceDiagram autonumber participant P as Partner App participant A as Appstle Subscriptions rect rgb(240, 248, 255) Note over P,A: Flow A — Partner initiates P->>A: POST /api/partner/{id}/connect
(shop_domain, callback_nonce, secret) A->>P: POST {your_base_url}/appstle/verify
(nonce check) P-->>A: { "verified": true } A-->>P: { "status": "pending_merchant_approval" } Note over P,A: Merchant approves in Appstle dashboard A->>P: POST {your_base_url}/appstle/approved
(access_token) end rect rgb(245, 245, 250) Note over P,A: Flow B — Appstle initiates A->>P: POST {your_base_url}/appstle/connect
(shop_domain, app, callback_url, nonce) P->>A: POST /api/partner/{id}/verify
(shop_domain, callback_nonce, secret) A-->>P: { "access_token": "..." } end ``` **Merchant approval:** When your app initiates a connection (Flow A), the merchant must approve it from their Appstle dashboard before you receive an API token. When the merchant initiates from Appstle's side (Flow B), the connection is approved instantly because the merchant is the one clicking "Connect." ## Getting started ### Step 1: Get onboarded To get started, reach out to the Appstle team at [support@appstle.com](mailto:support@appstle.com) with the information below. Our team will set up your partner account and send you your credentials. #### What you'll need to provide | # | Field | Required? | Description | Example | | - | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | | 1 | **App / Company Name** | Required | Your app or company name. Displayed to merchants when they browse available partner integrations in the Appstle dashboard. | `SearchPie` | | 2 | **Partner ID** | Required | A unique, lowercase slug that identifies your app in API URLs. Use only lowercase letters and hyphens. Once set, this cannot be changed. | `search-pie` | | 3 | **Base URL** | Required | The HTTPS base URL where Appstle will send callback requests (connect, verify, approved). Must be publicly accessible — Appstle will not call HTTP or localhost URLs. | `https://api.searchpie.com` | | 4 | **Contact Email** | Required | The email address where we'll send your Partner Secret and any onboarding follow-ups. Use a team email if possible — the secret is shown only once. | `dev-team@searchpie.com` | | 5 | **Authentication Mode** | Optional | How your API calls are authenticated. Choose one: **Partner Secret** (simpler — pass secret in a header) or **HMAC-SHA256** (more secure — sign each request). Defaults to Partner Secret if not specified. | `Partner Secret` | | 6 | **Connect Mode** | Optional | How merchant connections are established. Choose one: **Nonce Handshake** (full two-way verification — you receive an Appstle API key) or **Simple Token Exchange** (streamlined — you provide your own token for Appstle to call your API). Defaults to Nonce Handshake if not specified. | `Nonce Handshake` | | 7 | **Custom Endpoint Paths** | Optional | By default, Appstle calls `/appstle/connect` and `/appstle/verify` on your Base URL. If you need different paths (e.g., `/webhooks/appstle/connect`), specify them here. | `/webhooks/appstle/connect` | | 8 | **Sync Path** | Optional | If you want Appstle to push subscription data to your app (e.g., when contracts are created, updated, or cancelled), provide the path on your server where Appstle should send these payloads. | `/appstle/sync` | | 9 | **App Logo** | Optional | A square logo (PNG or SVG, at least 128×128px) displayed next to your app name in the merchant's Appstle dashboard. If not provided, a placeholder icon is used. | — | Not sure about some of these? Only the first four fields are required to get started. You can always reach out to [support@appstle.com](mailto:support@appstle.com) to change your authentication mode, connect mode, or add a sync path later. **Recommended default for new partners: Simple Token Exchange + Partner Secret.** This is the lowest-friction setup — your app generates a single access token per merchant, hands it to Appstle, and authenticates calls with an `X-Partner-Secret` header. No nonce storage, no HMAC computation, no `/appstle/verify` endpoint to implement. Pick this unless you specifically need Appstle to call your API on behalf of a merchant (use Nonce Handshake) or your security review mandates request signing (use HMAC-SHA256). #### API namespaces — what you call vs. what is internal Three URL namespaces appear in this codebase. **As a third-party partner, you only ever call the first one.** The others exist for Appstle's merchant portal and inter-app integrations and are documented here so the surface area is unambiguous: | Namespace | Who calls it | Purpose | | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `/api/partner/...` | **You** (the partner) | Connect, verify, disconnect, status. Authenticated with your Partner Secret or HMAC. | | `/api/integrations/partner/...` | Appstle merchant-portal UI | Drives the merchant-facing "Connect / Disconnect" buttons in the Appstle dashboard. Session-authenticated; not part of the public partner API. | | `/api/integrations/callback/...` | Other Appstle apps | Receiver-side callbacks for app-to-app integrations between Appstle products. Not used by third-party partners. | If you see an example referring to `/api/integrations/...`, it's an internal Appstle flow and doesn't apply to your integration. #### What you'll receive Once onboarded, you'll receive three values: | Credential | Example | Description | | ------------------ | ---------------------------------------- | ----------------------------------------------------------------------------- | | **Partner ID** | `search-pie` | Your unique identifier, as requested. Becomes part of the API URL. | | **Partner Secret** | `xK9mQ2vL...` (48 chars) | A secret key used to authenticate your API calls. Treat this like a password. | | **Base URL** | `https://subscription-admin.appstle.com` | Appstle's API base URL. Same for all partners. | Your Partner Secret is shown **only once** during onboarding. Copy it immediately and store it in a secure location (environment variable, secrets manager, etc.). If you lose it, contact Appstle to rotate it — the old secret will be invalidated immediately. Store your credentials as environment variables: ```bash .env theme={null} # never commit this file APPSTLE_PARTNER_ID=search-pie APPSTLE_PARTNER_SECRET=xK9mQ2vLa8nR3pY... # if using Partner Secret auth APPSTLE_HMAC_KEY=your-hmac-key-here # if using HMAC-SHA256 auth APPSTLE_BASE_URL=https://subscription-admin.appstle.com ``` ### Step 1b: Choose your authentication mode Appstle supports two ways to authenticate partner API calls. Your auth mode is configured during onboarding. #### Option A: Partner Secret (default) The simplest approach. Pass your secret in a header with every request: ``` X-Partner-Secret: your-partner-secret ``` That's it. No computation needed. Good for getting started quickly. #### Option B: HMAC-SHA256 A more secure approach where requests are signed with a shared HMAC key. Instead of sending the secret directly, you compute a signature over the request body. Headers required: ``` X-Partner-Timestamp: 1709856000 X-Partner-Signature: 5a3c1f2e9b8d7a6c... ``` How to compute the signature: 1. Get the current Unix timestamp (seconds, not milliseconds) 2. Concatenate the timestamp and the raw JSON request body: `timestamp + body` 3. Compute HMAC-SHA256 of that string using your HMAC key 4. Send the hex-encoded result in `X-Partner-Signature` **Timestamp validation:** Appstle rejects requests where the timestamp is more than **5 minutes** from the server's current time. Make sure your server clock is synced (NTP). ```javascript Node.js theme={null} const crypto = require('crypto'); function signRequest(body, hmacKey) { const timestamp = Math.floor(Date.now() / 1000).toString(); const data = timestamp + body; const signature = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', }; } // Usage const body = JSON.stringify({ shop_domain: 'cool-store.myshopify.com' }); const headers = signRequest(body, process.env.APPSTLE_HMAC_KEY); ``` ```python Python theme={null} import hmac import hashlib import time import json def sign_request(body: str, hmac_key: str) -> dict: timestamp = str(int(time.time())) data = timestamp + body signature = hmac.new( hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256, ).hexdigest() return { 'X-Partner-Timestamp': timestamp, 'X-Partner-Signature': signature, 'Content-Type': 'application/json', } # Usage body = json.dumps({"shop_domain": "cool-store.myshopify.com"}) headers = sign_request(body, os.environ["APPSTLE_HMAC_KEY"]) ``` ```bash curl theme={null} # Compute signature: HMAC-SHA256(timestamp + body, key) TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://subscription-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Which should I choose?** * **Partner Secret** — simpler to implement, fine for most integrations * **HMAC-SHA256** — better security (secret never sent over the wire), recommended for high-volume or security-sensitive integrations Both are equally supported. You can switch modes later by contacting Appstle. ### Step 1c: Choose your connect mode Appstle supports two ways to establish merchant connections. Your connect mode is configured during onboarding. #### Option A: Nonce Handshake (default) The full two-way verification flow described in this guide. Both sides verify each other using a one-time nonce. After the handshake, your app receives an **Appstle API key** (`apst_...`) to call Appstle's External API. **Best for:** Partners who want to read/write data in Appstle (subscription contracts, billing attempts, product swaps, etc.) #### Option B: Simple Token Exchange A streamlined flow where your app sends its own access token to Appstle (or Appstle calls your connect endpoint and you return one). No nonce, no verify endpoint needed. Appstle stores your token and uses it to call **your** API when needed. **Best for:** Partners where Appstle needs to call the partner's API (e.g., syncing data to the partner's platform), rather than the partner calling Appstle's API. **Key difference from Nonce Handshake:** In Simple Token Exchange, your app provides its own access token to Appstle. Appstle stores this token and uses it to push data to your API (via your `sync_path` — see [Data sync](#data-sync-push-model) below). Your app does **not** receive an Appstle API key in this mode. **Need both directions?** If you need to both push data to Appstle AND have Appstle push data to you, use the Nonce Handshake mode and provide a `sync_path` during onboarding. Contact [support@appstle.com](mailto:support@appstle.com) to discuss your use case. **How Simple Token Exchange works:** *Partner-initiated:* ```bash theme={null} curl -X POST "https://subscription-admin.appstle.com/api/partner/your-partner-id/connect" \ -H "X-Partner-Timestamp: 1709856000" \ -H "X-Partner-Signature: 5a3c1f2e..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "access_token": "your-apps-token-for-this-merchant" }' ``` Response: ```json theme={null} { "status": "pending_merchant_approval" } ``` Your `access_token` is stored securely but will not be activated until the merchant approves the connection from their Appstle dashboard. Once approved, Appstle calls your `/appstle/approved` endpoint to confirm (see [Handling the approval callback](#handling-the-approval-callback)). *Appstle-initiated:* Appstle calls your `/appstle/connect` endpoint with `{ "shop_domain": "..." }`. Your app responds with: ```json theme={null} { "success": true, "access_token": "your-apps-token-for-this-merchant" } ``` With Simple Token Exchange, your app does NOT receive an Appstle API key. If you also need to call Appstle's External API, use the Nonce Handshake mode instead. ### Step 2: Understand the callback nonce This section applies to Nonce Handshake mode only. If you're using Simple Token Exchange, skip to [Step 4](#step-4-implement-the-connect-flow-your-dashboard). Before implementing, you need to understand the **callback nonce** — it's the core security mechanism of the handshake. #### What is a callback nonce? A **nonce** (number used once) is a random, single-use string that proves both sides of the connection are who they claim to be. It prevents replay attacks and ensures the handshake can't be forged. #### Requirements | Requirement | Detail | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **Length** | At least 32 bytes (64 hex characters) | | **Randomness** | Must be **cryptographically random** — do NOT use `Math.random()`, `rand()`, timestamps, or UUIDs | | **Single-use** | Each nonce must be used exactly once, then deleted | | **Expiry** | Nonces expire after **5 minutes** on Appstle's side. Your storage should also expire them. | | **Storage** | Store temporarily with a TTL. Redis, DynamoDB, or any key-value store with expiry works. Database with a cleanup job is also fine. | #### How to generate a nonce Use your language's cryptographically secure random number generator. ```javascript Node.js theme={null} const crypto = require('crypto'); // Generate a 32-byte (64 hex character) cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```python Python theme={null} import secrets # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = secrets.token_hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```ruby Ruby theme={null} require 'securerandom' # Generate a 32-byte (64 hex character) cryptographically random nonce nonce = SecureRandom.hex(32) # Result: "a1b2c3d4e5f6...64 characters total" ``` ```php PHP theme={null} // Generate a 32-byte (64 hex character) cryptographically random nonce $nonce = bin2hex(random_bytes(32)); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```java Java theme={null} import java.security.SecureRandom; SecureRandom secureRandom = new SecureRandom(); byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); StringBuilder sb = new StringBuilder(64); for (byte b : bytes) { sb.append(String.format("%02x", b)); } String nonce = sb.toString(); // Result: "a1b2c3d4e5f6...64 characters total" ``` ```go Go theme={null} import ( "crypto/rand" "encoding/hex" ) bytes := make([]byte, 32) rand.Read(bytes) nonce := hex.EncodeToString(bytes) // Result: "a1b2c3d4e5f6...64 characters total" ``` **Common mistakes:** * `Math.random().toString(36)` — not cryptographically random, predictable * `uuid.v4()` — UUIDs are not designed as security tokens (some implementations use weak RNG) * `Date.now().toString()` — trivially guessable * Reusing nonces across multiple connect attempts Always use your language's `crypto` / `secrets` / `SecureRandom` module. #### How to store a nonce Store the nonce temporarily, keyed by shop domain, with a 5-minute expiry. Delete it after verification. ```javascript Node.js + Redis theme={null} const Redis = require('ioredis'); const redis = new Redis(); // Store nonce with 5-minute TTL async function storeNonce(shopDomain, nonce) { const key = `appstle:nonce:${shopDomain}`; await redis.set(key, nonce, 'EX', 300); // 300 seconds = 5 minutes } // Retrieve and delete nonce (single atomic operation) async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const storedNonce = await redis.get(key); if (!storedNonce || storedNonce !== nonceToCheck) { return false; } await redis.del(key); return true; } ``` ```python Python + database theme={null} from datetime import datetime, timedelta from your_app.models import PartnerNonce # your ORM model def store_nonce(shop_domain: str, nonce: str): # Delete any existing nonce for this shop (prevent duplicates) PartnerNonce.objects.filter(shop_domain=shop_domain).delete() PartnerNonce.objects.create( shop_domain=shop_domain, nonce=nonce, expires_at=datetime.utcnow() + timedelta(minutes=5), ) def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool: try: record = PartnerNonce.objects.get( shop_domain=shop_domain, nonce=nonce_to_check, expires_at__gt=datetime.utcnow(), # not expired ) record.delete() return True except PartnerNonce.DoesNotExist: return False ``` ### Step 3: Implement your endpoints Your app must expose two HTTP endpoints that Appstle calls during the connection handshake. The paths default to `/appstle/connect` and `/appstle/verify` but can be customized during onboarding. **Both endpoints must:** * Accept `POST` requests with a JSON body * Return JSON responses * Be accessible over **HTTPS** (Appstle will not call HTTP endpoints) * Respond within **10 seconds** (or the request will time out) * **Be idempotent.** Appstle may retry a callback on transient failure, and a merchant flipping connect/disconnect repeatedly will exercise the same endpoint with the same `(shop_domain, partnerId)` pair. Treat every call as an upsert keyed by `(shop_domain, partnerId)` — never blindly insert. The same rule applies to your `/appstle/approved` and `/appstle/disconnect` endpoints described later. #### Endpoint 1: `POST /appstle/connect` **When is this called?** Appstle calls this when a merchant initiates the connection from **Appstle's dashboard** (Flow B). **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "app": "subscriptions", "callback_url": "https://subscription-admin.appstle.com/api/partner/your-partner-id/verify", "callback_nonce": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" } ``` | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `shop_domain` | string | The merchant's Shopify domain (e.g. `cool-store.myshopify.com`) | | `app` | string | Always `"subscriptions"` — identifies which Appstle app is connecting | | `callback_url` | string | The exact URL your app must call to complete the handshake. **The `{partnerId}` embedded in this URL is *Appstle's* identifier for this Appstle app on your side, not your Partner ID.** Use the URL verbatim — don't parse or substitute the segment. | | `callback_nonce` | string | A one-time-use token generated by Appstle. **Expires in 5 minutes.** | **What should your app do?** 1. **Validate the shop** — check that this `shop_domain` exists in your system. If you don't recognize the shop, return an error. 2. **Store the nonce and callback URL** — save `callback_nonce` and `callback_url` associated with this `shop_domain`. You'll need them to complete the handshake. 3. **Call back to Appstle** — either immediately (auto-approve) or after merchant confirmation, call the `callback_url` to complete the connection. See [Completing the handshake](#completing-the-handshake-flow-b) below. 4. **Return a success response** — any `2xx` status code tells Appstle the request was received. ```javascript Node.js (Express) theme={null} const express = require('express'); const axios = require('axios'); const router = express.Router(); router.post('/appstle/connect', async (req, res) => { const { shop_domain, app, callback_url, callback_nonce } = req.body; // 1. Validate: does this shop exist in your system? const shop = await db.shops.findOne({ domain: shop_domain }); if (!shop) { return res.status(400).json({ error: 'Shop not found in our system' }); } // 2. Store the nonce and callback URL for this shop await db.pendingConnections.upsert({ shopDomain: shop_domain, callbackUrl: callback_url, callbackNonce: callback_nonce, createdAt: new Date(), expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes }); // 3. Option A: Auto-approve (call back immediately) try { const response = await axios.post(callback_url, { shop_domain: shop_domain, callback_nonce: callback_nonce, }, { headers: { 'X-Partner-Secret': process.env.APPSTLE_PARTNER_SECRET, 'Content-Type': 'application/json', }, }); if (response.data.verified && response.data.access_token) { // 4. Store the access token for this merchant await db.appstleTokens.upsert({ shopDomain: shop_domain, accessToken: response.data.access_token, connectedAt: new Date(), }); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } // 5. Return success to Appstle res.json({ success: true }); }); ``` #### Endpoint 2: `POST /appstle/verify` **When is this called?** Appstle calls this when a merchant initiates the connection from **your app's dashboard** (Flow A). Appstle is asking your app: "Did you actually send this nonce?" **What does it receive?** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f6...the-nonce-you-generated" } ``` | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------- | | `shop_domain` | string | The merchant's Shopify domain | | `callback_nonce` | string | The nonce your app originally sent in the `/connect` call | **What should your app do?** 1. Look up the stored nonce for this `shop_domain` 2. Compare the `callback_nonce` from the request against your stored nonce 3. If they match: delete the stored nonce (it's single-use) and return `{ "verified": true }` 4. If they don't match: return `{ "verified": false }` ```javascript Node.js (Express) theme={null} router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; // 1. Look up the stored nonce for this shop const isValid = await verifyAndDeleteNonce(shop_domain, callback_nonce); // 2. Return the result res.json({ verified: isValid }); }); ``` ### Step 4: Implement the connect flow (your dashboard) Now build the merchant-facing "Connect Appstle Subscriptions" button in your app's dashboard. #### Partner-initiated connect (Flow A) — step by step This is the flow where the merchant clicks "Connect Appstle" in **your** dashboard. The merchant initiates the connection from inside your app's UI. Store it keyed by `shop_domain` with a 5-minute TTL. Send the `shop_domain`, the `callback_nonce`, and your Partner Secret. Appstle also confirms the shop has Appstle Subscriptions installed. Payload: the `shop_domain` and the same `callback_nonce`. Confirm it matches, delete it, return `{ "verified": true }`. The connection is pending — no access token has been issued yet. They open **Settings → Partner Connections** and click **Approve**. The token is POSTed to YOUR `/appstle/approved` endpoint. Show "Connected!" to the merchant. You're done. **Full implementation (Node.js):** ```javascript theme={null} const crypto = require('crypto'); const axios = require('axios'); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL; // https://subscription-admin.appstle.com // Called when merchant clicks "Connect Appstle" in your dashboard async function connectToAppstle(shopDomain) { // Step 2: Generate a cryptographically random nonce const nonce = crypto.randomBytes(32).toString('hex'); // Store it so your /appstle/verify endpoint can look it up later await storeNonce(shopDomain, nonce); // see nonce storage examples above // Step 3: Call Appstle's partner connect endpoint const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json', }, } ); // Steps 4-6 happen automatically (Appstle calls your /appstle/verify) // Step 7: Appstle returns pending status — token is NOT delivered yet const { status } = response.data; if (status === 'pending_merchant_approval') { // The merchant needs to approve in their Appstle dashboard. // Once approved, Appstle will POST the token to your /appstle/approved endpoint. await markConnectionPending(shopDomain); return { pending: true }; } throw new Error('Connection failed'); } ``` **curl equivalent:** ```bash theme={null} curl -X POST "https://subscription-admin.appstle.com/api/partner/search-pie/connect" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890" }' ``` **Success response:** ```json theme={null} { "status": "pending_merchant_approval" } ``` **What happens next?** The merchant will see a "Pending Request" in their Appstle dashboard under **Settings → Partner Connections**. (This menu appears automatically once a partner initiates a connection request — it is not visible before any partner has connected.) When they click "Approve," Appstle creates a scoped API key and delivers it to your `/appstle/approved` endpoint (see [Handling the approval callback](#handling-the-approval-callback) below). Pending requests expire after **30 days** if not acted on. **Deep link to approval screen:** You can redirect the merchant directly to the approval screen to minimize friction: ``` https://admin.shopify.com/store/{shop-handle}/apps/subscription-by-rhem/settings/partner-connections ``` Replace `{shop-handle}` with the merchant's store handle (the part before `.myshopify.com`). This takes them straight to the pending connection for one-click approval. You can trigger this redirect in your UI immediately after receiving the `pending_merchant_approval` response. #### Appstle-initiated connect (Flow B) — step by step This is the flow where the merchant clicks "Connect" in **Appstle's** dashboard. The connection is initiated from inside Appstle, not your UI. Internal Appstle call — your app is not involved yet. Payload: `shop_domain`, `app`, `callback_url`, and `callback_nonce`. Persist them keyed by `shop_domain` for the verify step. Send `shop_domain`, `callback_nonce`, and your Partner Secret. The `access_token` is returned in the response body. Connection complete. ##### Completing the handshake (Flow B) After your `/appstle/connect` endpoint receives the nonce and callback URL, your app completes the connection by calling Appstle's verify endpoint: ```bash theme={null} curl -X POST "https://subscription-admin.appstle.com/api/partner/search-pie/verify" \ -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com", "callback_nonce": "the-nonce-appstle-sent-in-the-connect-call" }' ``` **Success response:** ```json theme={null} { "verified": true, "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Failed response (nonce expired or mismatched):** ```json theme={null} { "verified": false } ``` You must call the verify endpoint within **5 minutes** of receiving the nonce. After that, the nonce expires and the merchant will need to try again. ## Using the API token After a successful connection, your app has an `access_token` (prefixed with `apst_`). For **Appstle-initiated connections** (Flow B), the token is returned immediately in the verify response. For **partner-initiated connections** (Flow A), the token is delivered asynchronously to your `/appstle/approved` endpoint after the merchant approves (see [Handling the approval callback](#handling-the-approval-callback) below). Use this token exactly like a merchant API key — pass it in the `X-API-Key` header: ```bash theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/{customerId}" \ -H "X-API-Key: apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" ``` ### Token properties | Property | Detail | | -------------- | ---------------------------------------------------------------------------------------------- | | **Format** | Starts with `apst_` followed by 40 alphanumeric characters | | **Scope** | One token per merchant per partner | | **Permission** | `READ_ONLY` or `READ_WRITE` (set during partner onboarding) | | **Billing** | Partner tokens **bypass the paid API plan** — merchants are never billed for partner API usage | | **Revocation** | Revoked instantly when the merchant disconnects or uninstalls Appstle | | **Expiry** | Tokens do not expire on their own. They remain valid until explicitly revoked. | ### Available endpoints Partner tokens grant access to the same [External API endpoints](/subscription/integration-guide) as merchant API keys: * **Subscription contracts** — `GET /api/external/v2/subscription-contract-details` * **Customer subscriptions** — `GET /api/external/v2/subscription-customers/{customerId}` * **Update subscription status** — `PUT /api/external/v2/subscription-contracts-update-status` *(requires READ\_WRITE)* * **Skip/Reschedule orders** — `POST /api/external/v2/subscription-billing-attempts/skip-order/{id}` *(requires READ\_WRITE)* * **Update line items** — `PUT /api/external/v2/subscription-contracts-update-line-item` *(requires READ\_WRITE)* * **Apply discounts** — `POST /api/external/v2/subscription-contracts-apply-discount` *(requires READ\_WRITE)* * And all other `/api/external/v2/*` endpoints See the full [Integration guide](/subscription/integration-guide) for complete endpoint documentation. ## Data sync (push model) Some integrations work best when **Appstle pushes data to your app**, rather than your app pulling from Appstle's API. For example, a search platform might need Appstle to push subscription data so it can be indexed alongside other store data. ### How it works During onboarding, you can configure a `sync_path` on your server (e.g., `/appstle/sync`). When subscription events occur (new subscriptions, cancellations, billing attempts, etc.), Appstle calls your endpoint with the relevant data. | Config Field | Example | Description | | ----------------- | --------------------- | ---------------------------------------------------- | | `sync_path` | `/appstle/sync` | Your endpoint where Appstle pushes subscription data | | `disconnect_path` | `/appstle/disconnect` | Your endpoint called when a merchant disconnects | ### Authentication When Appstle calls your endpoints, it authenticates using the auth mode configured for your partner: * **Partner Secret mode:** No additional headers (your endpoints are responsible for validating the source — consider IP allowlisting) * **HMAC-SHA256 mode (recommended):** Appstle signs every request with `X-Partner-Timestamp` and `X-Partner-Signature` headers. Your app should verify the HMAC signature to confirm the request came from Appstle. **Verifying incoming HMAC signatures (Node.js):** ```javascript theme={null} const crypto = require('crypto'); function verifyAppstleSignature(req, hmacKey) { const timestamp = req.headers['x-partner-timestamp']; const signature = req.headers['x-partner-signature']; if (!timestamp || !signature) return false; // Reject if timestamp is more than 5 minutes old const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) return false; // Compute expected signature const data = timestamp + req.rawBody; // make sure you capture raw body const expected = crypto .createHmac('sha256', hmacKey) .update(data) .digest('hex'); // Constant-time comparison return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ### Pull vs push — which model do I need? | Model | Connect Mode | Your App Calls Appstle? | Appstle Calls Your App? | Use Case | | ------------------ | ---------------------------- | ------------------------- | --------------------------------- | --------------------------------------------------- | | **Pull** (default) | Nonce Handshake | Yes (via `apst_` API key) | No | Helpdesks, CRMs reading subscription data on demand | | **Push** | Simple Token Exchange | No | Yes (via your token + sync\_path) | Search platforms, analytics tools that index data | | **Bidirectional** | Nonce Handshake + sync\_path | Yes | Yes | Full two-way integrations | ## Disconnecting ### Merchant disconnects from Appstle Merchants can disconnect your integration anytime from **Appstle Dashboard → Settings → Partner Connections**. When they do: * Your API token for that merchant is **revoked immediately** * Subsequent API calls will return `401 Unauthorized` * **Appstle sends a disconnect webhook** to your app (if you configured a `disconnect_path` during onboarding — see below) * Your app should handle this gracefully and show a "Reconnect" option **Best practice:** In your API client, check for `401` responses and update your UI to show the connection as disconnected: ```javascript theme={null} async function callAppstleApi(shopDomain, endpoint) { const token = await getAccessToken(shopDomain); try { const response = await axios.get(`${APPSTLE_BASE}${endpoint}`, { headers: { 'X-API-Key': token }, }); return response.data; } catch (err) { if (err.response?.status === 401) { // Token was revoked — merchant disconnected await markAsDisconnected(shopDomain); throw new Error('Appstle connection was revoked. Merchant needs to reconnect.'); } throw err; } } ``` ### Disconnect webhook (first-class endpoint) Configure a `disconnect_path` during onboarding (defaults to `/appstle/disconnect`). Appstle calls this whenever a merchant disconnects — from the Appstle dashboard, from your app, or by uninstalling Appstle entirely. **You should implement this endpoint for every integration** — it is the only reliable signal that the merchant has revoked access on Appstle's side. Polling `401` responses as a fallback works but lags behind. **Request body from Appstle:** ```json theme={null} { "shop_domain": "cool-store.myshopify.com" } ``` If your partner uses HMAC-SHA256 auth, the webhook includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. **Your endpoint must:** 1. **Look up the connection without filtering on status.** Don't `WHERE status = 'active'` — if the merchant rapid-clicks disconnect twice, the second call may arrive when the row is already inactive. Find by `(shop_domain, partnerId)` only. 2. **Revoke the Appstle access token idempotently.** If the token is already revoked or absent, return success — don't error. Revocation must be safe to call repeatedly. 3. **Mark the local connection inactive.** Clear or null out the stored Appstle token so subsequent API calls don't try to use it. 4. **Return `2xx` even when there was nothing to do.** A no-op disconnect is a successful disconnect from Appstle's perspective. ```javascript Node.js (Express) theme={null} router.post('/appstle/disconnect', async (req, res) => { const { shop_domain } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } // 1. Status-agnostic lookup — don't filter on .where({ status: 'active' }) const connection = await db.connections.findOne({ shopDomain: shop_domain }); if (connection) { // 2. Idempotent token revoke — clearing a null token is a no-op await db.appstleTokens.delete({ shopDomain: shop_domain }); // 3. Mark inactive (upsert-style — safe if already inactive) await db.connections.update( { shopDomain: shop_domain }, { status: 'disconnected', disconnectedAt: new Date() } ); } // 4. Always 2xx — even if nothing was found res.json({ success: true }); }); ``` ```python Python (Flask) theme={null} @app.route("/appstle/disconnect", methods=["POST"]) def appstle_disconnect(): shop_domain = request.json["shop_domain"] # 1. Find without filtering on status connection = Connection.query.filter_by(shop_domain=shop_domain).first() if connection: # 2. Idempotent token revoke AppstleToken.query.filter_by(shop_domain=shop_domain).delete() # 3. Mark inactive (upsert semantics) connection.status = "disconnected" connection.disconnected_at = datetime.utcnow() db.session.commit() # 4. Always 2xx return jsonify({"success": True}) ``` This is a best-effort notification — your app should also handle `401` responses from the Appstle API as a fallback signal that the connection was revoked. ### Partner disconnects programmatically Your app can disconnect a merchant using your partner authentication (Partner Secret or HMAC-SHA256): ```bash Partner Secret theme={null} curl -X POST "https://subscription-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Secret: YOUR_PARTNER_SECRET" \ -H "Content-Type: application/json" \ -d '{ "shop_domain": "cool-store.myshopify.com" }' ``` ```bash HMAC-SHA256 theme={null} TIMESTAMP=$(date +%s) BODY='{"shop_domain":"cool-store.myshopify.com"}' SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "your-hmac-key" | awk '{print $2}') curl -X POST "https://subscription-admin.appstle.com/api/partner/your-partner-id/disconnect" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" \ -H "Content-Type: application/json" \ -d "$BODY" ``` **Response:** ```json theme={null} { "success": true } ``` ### Check connection status `GET /api/partner/{partnerId}/status?shop_domain=...` is the **authoritative source of truth** for whether a merchant is connected. If your UI shows a "Connected" badge, derive it from this endpoint — not from whether you happen to have a stored API key locally. **Why this matters:** Older integrations sometimes inferred "connected" from the presence of a per-app API key column in their own database. That column is now a deprecated fallback — it can be stale (key revoked on Appstle's side, your row never updated) and it can't represent `pending_merchant_approval` or `rejected`. Always call `/status` before showing connection state to the merchant or making business decisions based on it. Partners can authenticate with their Partner Secret or HMAC signature: ```bash theme={null} # With Partner Secret curl -X GET "https://subscription-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Secret: your-partner-secret" # With HMAC curl -X GET "https://subscription-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \ -H "X-Partner-Timestamp: $TIMESTAMP" \ -H "X-Partner-Signature: $SIGNATURE" ``` **Response (active connection):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "active", "connected_at": "2026-03-07T20:30:00Z" } ``` **Response (pending merchant approval):** ```json theme={null} { "partner_id": "your-partner-id", "shop_domain": "cool-store.myshopify.com", "status": "pending_merchant_approval" } ``` **All possible status values:** | Status | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | Connected and working — API token is valid | | `pending_merchant_approval` | Partner-initiated connect is awaiting merchant approval | | `rejected` | Merchant rejected the connection request | | `expired` | A pending request expired (30-day window) without merchant action — partner must initiate a new connection | | `not_connected` | No connection record exists for this partner + shop, or the connection was previously terminated (by merchant, by partner, or by uninstall). In both cases the token is revoked and your app would need to initiate a new connection. | Use the status endpoint to poll for approval if your app doesn't implement the `/appstle/approved` callback. Poll every 30–60 seconds after initiating a connect. Once the status changes from `pending_merchant_approval` to `active`, your token has been delivered via the approval callback (or you can request it again). ## Handling the approval callback When a merchant approves a partner-initiated connection, Appstle delivers the API token by calling an endpoint on your server. This applies to **partner-initiated connections only** — Appstle-initiated connections (Flow B) return the token immediately. ### Endpoint: `POST /appstle/approved` The path defaults to `/appstle/approved` but can be customized during onboarding (configured as `approval_callback_path`). Your `/appstle/approved` endpoint **must accept unauthenticated POST requests** from Appstle's servers. Do not put authentication middleware (e.g., JWT validation, API key checks) on this endpoint — Appstle will not send your app's auth credentials when calling this callback. If you need to verify the request is from Appstle, use HMAC-SHA256 authentication mode — when enabled, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) you can verify. **Request body from Appstle (Nonce Handshake mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012" } ``` **Request body from Appstle (Simple Token Exchange mode):** ```json theme={null} { "shop_domain": "cool-store.myshopify.com", "status": "approved" } ``` In Simple Token Exchange mode, your app already provided its own token during the connect call. The approval callback simply confirms the connection is now active — Appstle will start using your token for API calls. **Expected response:** Return any `2xx` status code with a JSON body (e.g., `{ "success": true }`). If your endpoint returns a non-2xx status (e.g., `401 Unauthorized`), the connection is still approved on Appstle's side, but your app won't know — see [What if the callback fails?](#what-if-the-callback-fails) below. If your partner uses HMAC-SHA256 auth, the callback includes signed headers (`X-Partner-Timestamp`, `X-Partner-Signature`) so you can verify it came from Appstle. ```javascript Node.js theme={null} router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; // Optional: verify HMAC signature if using HMAC auth // if (!verifyAppstleSignature(req, HMAC_KEY)) { // return res.status(401).json({ error: 'Invalid signature' }); // } if (access_token) { // Nonce Handshake mode — store the Appstle API token await saveToken(shop_domain, access_token); console.log(`Connection approved for ${shop_domain} — token received`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active await markConnectionActive(shop_domain); console.log(`Connection approved for ${shop_domain} — our token is now active`); } res.json({ success: true }); }); ``` ```python Python theme={null} @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active mark_connection_active(shop_domain) return jsonify({"success": True}) ``` ### What if the callback fails? If your endpoint is unreachable or returns an error, the connection is still approved on Appstle's side. The API token exists and is valid. Your app can: 1. **Poll the status endpoint** — check `GET /api/partner/{id}/status?shop_domain=...` until the status is `active` 2. **Retry from Appstle's side** — currently, Appstle does not automatically retry the callback. Contact support if you need the token re-delivered. ### What if the merchant rejects? If the merchant clicks "Reject," the connection status changes to `rejected` and Appstle notifies your app via the disconnect webhook (if configured). Your app should handle this gracefully — show the merchant that the connection was not approved. ## Error handling All partner endpoints return structured error responses: ```json theme={null} { "type": "https://subscription-admin.appstle.com/problem", "title": "Bad Request", "status": 400, "detail": "UserGeneratedError:Active connection already exists. Disconnect first.", "errorKey": "ALREADY_CONNECTED" } ``` ### Error codes | Error Code | HTTP Status | When It Happens | What To Do | | --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PARTNER_NOT_FOUND` | 400 | Your Partner ID is wrong, or the partner has been deactivated | Double-check your Partner ID. Contact Appstle if unexpected. | | `TOKEN_INVALID` | 400 | Auth failed: `X-Partner-Secret` is wrong, or HMAC signature is invalid, or timestamp is >5 min off | Verify your secret or HMAC key. Check for trailing whitespace. For HMAC: ensure server clock is synced (NTP) and you're signing `timestamp + body` exactly. | | `SHOP_NOT_FOUND` | 400 | The shop doesn't have Appstle Subscriptions installed | Tell the merchant to install Appstle Subscriptions first. | | `ALREADY_CONNECTED` | 400 | An active connection already exists for this partner + shop | Call disconnect first, then reconnect. Or skip — you're already connected. | | `VERIFICATION_FAILED` | 400 | Nonce didn't match, expired (>5 min), or your `/verify` endpoint returned `false` | Generate a fresh nonce and try again. Check your nonce storage logic. | | `NOT_CONNECTED` | 400 | Trying to disconnect or check status, but no active connection exists | The merchant may have already disconnected from their side. | | `PARTNER_UNREACHABLE` | 400 | Appstle couldn't reach your `/appstle/connect` or `/appstle/verify` endpoint | Check your endpoint URL is correct, HTTPS, and publicly accessible. Check your server logs. | | `UNEXPECTED_ROLLBACK` | 500 | Your endpoint returned success, but Appstle's transaction was silently rolled back. Manifests in logs as `UnexpectedRollbackException` / `Transaction silently rolled back because it has been marked as rollback-only`. | Common footgun for partners running on transactional frameworks: an inner write throws and gets caught by your handler, but the surrounding transaction has already been marked rollback-only — so the outer commit fails with no visible error from your business logic. Fix is in your code: either let the inner exception propagate, or perform the write in a fresh inner transaction. Don't swallow exceptions inside a transactional boundary. | #### Idempotency requirements — recap The integration framework relies on partners treating callbacks as **at-least-once**. Concretely: * **Connect / approval callbacks:** upsert by `(shop_domain, partnerId)`. Two `/appstle/approved` calls for the same shop must produce the same end state, not two rows. * **Disconnect callback:** find the connection without filtering on status; revoke tokens idempotently; return `2xx` even when there is nothing to do. * **Status reads:** safe by definition — no side effects. If your code is built on "this only ever fires once", expect bugs the first time the merchant flips connect/disconnect quickly or the first time a network blip triggers an Appstle retry. ## Security checklist Before going live, verify all of these: * [ ] **Partner Secret / HMAC Key** is stored in environment variables or a secrets manager — not hardcoded in source code * [ ] **Nonces** are generated using a cryptographically secure random generator (`crypto.randomBytes`, `secrets.token_hex`, `SecureRandom`, etc.) * [ ] **Nonces** are stored with a TTL (≤ 5 minutes) and deleted after verification * [ ] **Nonces** are compared using a constant-time comparison to prevent timing attacks (most frameworks do this by default for string equality) * [ ] **Endpoints** are served over **HTTPS** — Appstle will not call HTTP endpoints * [ ] **`shop_domain`** is validated in your `/appstle/connect` and `/appstle/verify` endpoints — reject domains you don't recognize * [ ] **Access tokens** are stored encrypted at rest (or in a secrets manager) * [ ] **401 responses** are handled gracefully — show a "Reconnect" option, don't break silently * [ ] **Error responses** from Appstle are logged for debugging * [ ] **(HMAC only)** Server clock is synced via NTP — timestamps more than 5 minutes off will be rejected * [ ] **(If using disconnect webhook)** Your `/appstle/disconnect` endpoint cleans up stored tokens and marks the connection as inactive ## Complete example: partner-initiated flow ```javascript Node.js theme={null} // appstle-partner.js const express = require('express'); const crypto = require('crypto'); const axios = require('axios'); const Redis = require('ioredis'); const router = express.Router(); const redis = new Redis(process.env.REDIS_URL); const PARTNER_ID = process.env.APPSTLE_PARTNER_ID; const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET; const APPSTLE_BASE = process.env.APPSTLE_BASE_URL || 'https://subscription-admin.appstle.com'; const NONCE_TTL = 300; // 5 minutes in seconds // ────────────────────────────────────────────── // Nonce helpers // ────────────────────────────────────────────── async function storeNonce(shopDomain, nonce) { await redis.set(`appstle:nonce:${shopDomain}`, nonce, 'EX', NONCE_TTL); } async function verifyAndDeleteNonce(shopDomain, nonceToCheck) { const key = `appstle:nonce:${shopDomain}`; const stored = await redis.get(key); if (!stored || stored !== nonceToCheck) return false; await redis.del(key); return true; } // ────────────────────────────────────────────── // Token storage (use your database in production) // ────────────────────────────────────────────── async function saveToken(shopDomain, accessToken) { // In production: encrypt the token before storing await redis.set(`appstle:token:${shopDomain}`, accessToken); } async function getToken(shopDomain) { return redis.get(`appstle:token:${shopDomain}`); } // ────────────────────────────────────────────── // Flow A: Partner-initiated connect // Called when merchant clicks "Connect Appstle" in YOUR dashboard // ────────────────────────────────────────────── router.post('/connect-appstle', async (req, res) => { const { shopDomain } = req.body; try { // 1. Generate nonce const nonce = crypto.randomBytes(32).toString('hex'); await storeNonce(shopDomain, nonce); // 2. Call Appstle const response = await axios.post( `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`, { shop_domain: shopDomain, callback_nonce: nonce }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' } } ); // 3. Connection is now pending merchant approval if (response.data.status === 'pending_merchant_approval') { // Mark as pending in your system — show the merchant a "waiting for approval" state await redis.set(`appstle:pending:${shopDomain}`, 'true'); return res.json({ pending: true, message: 'Waiting for merchant to approve in Appstle dashboard' }); } res.status(400).json({ error: 'Connection failed' }); } catch (err) { const detail = err.response?.data?.detail || err.message; console.error('Appstle connect failed:', detail); res.status(400).json({ error: detail }); } }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/verify // Called BY Appstle during Flow A to verify your nonce // ────────────────────────────────────────────── router.post('/appstle/verify', async (req, res) => { const { shop_domain, callback_nonce } = req.body; const verified = await verifyAndDeleteNonce(shop_domain, callback_nonce); res.json({ verified }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/approved // Called BY Appstle when merchant approves a partner-initiated connection // ────────────────────────────────────────────── router.post('/appstle/approved', async (req, res) => { const { shop_domain, access_token, status } = req.body; if (access_token) { // Nonce Handshake mode — Appstle is delivering our API token await saveToken(shop_domain, access_token); await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Token received for ${shop_domain}`); } else if (status === 'approved') { // Simple Token Exchange mode — our token is now active on Appstle's side await redis.del(`appstle:pending:${shop_domain}`); console.log(`Approved! Our token is now active for ${shop_domain}`); } res.json({ success: true }); }); // ────────────────────────────────────────────── // Endpoint: POST /appstle/connect // Called BY Appstle during Flow B (Appstle-initiated) // ────────────────────────────────────────────── router.post('/appstle/connect', async (req, res) => { const { shop_domain, callback_url, callback_nonce } = req.body; // Verify the shop exists in your system // const shop = await db.shops.findOne({ domain: shop_domain }); // if (!shop) return res.status(400).json({ error: 'Unknown shop' }); // Auto-approve: immediately call back to complete the handshake // (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try { const response = await axios.post(callback_url, { shop_domain, callback_nonce, }, { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' }, }); if (response.data.verified && response.data.access_token) { await saveToken(shop_domain, response.data.access_token); } } catch (err) { console.error('Failed to complete Appstle handshake:', err.message); } res.json({ success: true }); }); module.exports = router; ``` ```python Python theme={null} # appstle_partner.py import os import secrets import redis import requests from flask import Flask, request, jsonify app = Flask(__name__) r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379")) PARTNER_ID = os.environ["APPSTLE_PARTNER_ID"] PARTNER_SECRET = os.environ["APPSTLE_PARTNER_SECRET"] APPSTLE_BASE = os.environ.get("APPSTLE_BASE_URL", "https://subscription-admin.appstle.com") NONCE_TTL = 300 # 5 minutes def store_nonce(shop_domain, nonce): r.set(f"appstle:nonce:{shop_domain}", nonce, ex=NONCE_TTL) def verify_and_delete_nonce(shop_domain, nonce_to_check): key = f"appstle:nonce:{shop_domain}" stored = r.get(key) if not stored or stored.decode() != nonce_to_check: return False r.delete(key) return True def save_token(shop_domain, access_token): r.set(f"appstle:token:{shop_domain}", access_token) # ── Flow A: Partner-initiated connect ── @app.route("/connect-appstle", methods=["POST"]) def connect_appstle(): shop_domain = request.json["shopDomain"] # 1. Generate nonce nonce = secrets.token_hex(32) store_nonce(shop_domain, nonce) # 2. Call Appstle resp = requests.post( f"{APPSTLE_BASE}/api/partner/{PARTNER_ID}/connect", json={"shop_domain": shop_domain, "callback_nonce": nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) resp.raise_for_status() data = resp.json() # 3. Connection is pending merchant approval if data.get("status") == "pending_merchant_approval": r.set(f"appstle:pending:{shop_domain}", "true") return jsonify({"pending": True, "message": "Waiting for merchant to approve in Appstle dashboard"}) return jsonify({"error": "Connection failed"}), 400 # ── Endpoint: POST /appstle/verify (called BY Appstle during Flow A) ── @app.route("/appstle/verify", methods=["POST"]) def appstle_verify(): body = request.json verified = verify_and_delete_nonce(body["shop_domain"], body["callback_nonce"]) return jsonify({"verified": verified}) # ── Endpoint: POST /appstle/approved (called BY Appstle when merchant approves) ── @app.route("/appstle/approved", methods=["POST"]) def appstle_approved(): body = request.json shop_domain = body["shop_domain"] access_token = body.get("access_token") status = body.get("status") if access_token: # Nonce Handshake mode — store the Appstle API token save_token(shop_domain, access_token) elif status == "approved": # Simple Token Exchange mode — our token is now active pass # mark connection as active in your DB r.delete(f"appstle:pending:{shop_domain}") return jsonify({"success": True}) # ── Endpoint: POST /appstle/connect (called BY Appstle during Flow B) ── @app.route("/appstle/connect", methods=["POST"]) def appstle_connect(): body = request.json shop_domain = body["shop_domain"] callback_url = body["callback_url"] callback_nonce = body["callback_nonce"] # Auto-approve: call back immediately # (Flow B doesn't need merchant approval — merchant initiated it from Appstle) try: resp = requests.post( callback_url, json={"shop_domain": shop_domain, "callback_nonce": callback_nonce}, headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"}, ) data = resp.json() if data.get("verified") and data.get("access_token"): save_token(shop_domain, data["access_token"]) except Exception as e: app.logger.error(f"Handshake failed: {e}") return jsonify({"success": True}) ``` ## FAQ Yes. Each partner gets its own scoped API token. Merchants can connect as many partners as they want. The tokens are completely independent. All active partner connections for that shop are automatically disconnected. Your tokens will stop working (401 responses). Yes — contact the Appstle team. New connections will use the updated permission, but existing connections keep their original permission until reconnected. Partner tokens share the same rate limits as regular API keys. If you receive a `429 Too Many Requests`, implement exponential backoff. Use a Shopify development store with Appstle Subscriptions installed. The partner integration works identically in development and production. You can use a tool like [ngrok](https://ngrok.com) to expose your local endpoints to the internet for testing. The merchant simply needs to click "Connect" again. A new nonce will be generated. Old nonces are automatically cleaned up. Yes. Flow A is for when the merchant connects from your dashboard. Flow B is for when they connect from Appstle's dashboard. Both are needed for a complete integration. You also need the `/appstle/approved` endpoint to receive API tokens after merchant approval (Flow A). No. A new connect handshake requires the merchant to initiate it from one of the dashboards. This is by design — merchants must explicitly authorize each connection. For security and trust. When your app initiates a connection, the merchant hasn't explicitly agreed on Appstle's side. The approval step ensures merchants consciously grant API access to partner apps. Appstle-initiated connections (Flow B) skip this step because the merchant is already clicking "Connect" in the Appstle dashboard. Pending connection requests expire after **30 days**. If the merchant doesn't approve or reject within that window, the request expires and your app will need to initiate a new connection. The status changes to `rejected` and your app is notified via the disconnect webhook (if configured). The merchant can be asked to reconnect later if they change their mind — your app can initiate a new connection request. ## Need help? * **Partner onboarding & technical support:** [support@appstle.com](mailto:support@appstle.com) * **Integration guide:** [Third-party integration guide](/subscription/integration-guide) (for direct API key usage) # Get started with Appstle Subscriptions API Source: https://developers.appstle.com/subscription/quickstart Create your Appstle API key, make your first authenticated request, and retrieve a customer's subscriptions in under five minutes with working curl examples. This guide walks you through the three steps needed to make your first successful API call: creating an API key, sending an authenticated request, and reading the response. By the end you will have verified that your credentials work and seen what subscription data looks like. You need an active Appstle Subscriptions installation on a Shopify store to follow this guide. If you do not have one yet, install the app from the Shopify App Store first. ## Step 1 — Get your API key Log in to your Appstle admin panel and go to **Settings → API Key Management**. Click **Create New Key**. Give it a name like `Quickstart Test` so you can identify it later. The key is shown only once. Copy it now — you cannot retrieve it again. It will look like `apst_abc123...`. Keep this key secret. Do not commit it to source control or include it in client-side code. For production use, store it in an environment variable or a secrets manager. ## Step 2 — Make your first request The simplest useful call checks whether a specific customer has any active subscriptions. Replace `YOUR_API_KEY` with your key and `CUSTOMER_ID` with a numeric Shopify customer ID from your store. ```bash curl theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/CUSTOMER_ID" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript Node.js theme={null} const customerId = 'CUSTOMER_ID'; const response = await fetch( `https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/${customerId}`, { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); const contractIds = await response.json(); console.log(contractIds); // [5234567890, 5234567891] ``` ```python Python theme={null} import os import requests customer_id = 'CUSTOMER_ID' response = requests.get( f'https://subscription-admin.appstle.com/api/external/v2/subscription-customers/valid/{customer_id}', headers={'X-API-Key': os.environ['APPSTLE_API_KEY']}, ) contract_ids = response.json() print(contract_ids) # [5234567890, 5234567891] ``` ## Step 3 — Check the response A successful response returns an HTTP `200` with a JSON array of Shopify subscription contract IDs for that customer: ```json theme={null} [5234567890, 5234567891] ``` An empty array means the customer has no active subscriptions. If you receive a `401`, double-check that you copied the API key correctly and that it has not been revoked. | Response | Meaning | | ---------------- | ------------------------------------------------------------------ | | `200` with array | Customer has subscriptions — the array contains their contract IDs | | `200` with `[]` | Customer exists but has no active subscriptions | | `401` | API key is missing or invalid | | `404` | No customer found with that ID | ## Step 4 — Retrieve full subscription details Now that you have a contract ID, you can fetch the complete details for that subscription: ```bash curl theme={null} curl -X GET \ "https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details?contractId=5234567890" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript Node.js theme={null} const contractId = 5234567890; const response = await fetch( `https://subscription-admin.appstle.com/api/external/v2/subscription-contract-details?contractId=${contractId}`, { headers: { 'X-API-Key': process.env.APPSTLE_API_KEY, }, } ); const subscription = await response.json(); console.log(subscription.status); // "ACTIVE" console.log(subscription.nextBillingDate); // "2026-02-15" console.log(subscription.customer.email); // "customer@example.com" ``` The response includes the subscription status, next billing date, products, customer details, shipping address, payment method, and applied discounts. ## What's next? Full walkthrough of subscription management, product operations, discounts, shipping, and more. Receive real-time notifications when subscriptions change. Automate subscription workflows without writing backend code. # Automate subscriptions with Shopify Flow Source: https://developers.appstle.com/subscription/shopify-flow Use Appstle's Shopify Flow triggers and actions to automate subscription workflows without API tokens — everything runs in Shopify's authenticated context. Appstle Subscriptions integrates natively with [Shopify Flow](https://www.shopify.com/flow), Shopify's built-in automation platform. You can build powerful subscription automation — pausing after failed payments, applying loyalty discounts, swapping seasonal products — without managing API keys or writing backend code. Flow runs within Shopify's authenticated context, so no credentials are required. Shopify Flow is available on **Shopify Basic plan and above**. The Appstle Flow integration must be enabled in your Appstle admin before creating workflows. ## Getting started In the Appstle admin, go to **Settings → Integrations → Shopify Flow** and enable the integration. Navigate to the [Shopify Flow editor](https://admin.shopify.com/store/YOUR_STORE/apps/flow) in your Shopify admin. Click **Create workflow**, choose an Appstle trigger, add conditions if needed, and attach Appstle actions. ## Triggers Triggers fire automatically when subscription events occur. All triggers include the full subscription and customer property sets described below. ### Subscription lifecycle | Trigger | Description | | ---------------------- | -------------------------------------------------------------------------- | | Subscription Created | Fires when a new subscription contract is created | | Subscription Activated | Fires when a subscription is activated from paused or cancelled status | | Subscription Cancelled | Fires when a subscription is cancelled | | Subscription Paused | Fires when a subscription is paused | | Subscription Updated | Fires when subscription details change (products, quantity, address, etc.) | ### Billing events | Trigger | Description | | ---------------------------- | --------------------------------------------------------------------------------- | | Subscription Billing Success | Fires when a billing attempt succeeds | | Subscription Billing Failure | Fires when a billing attempt fails | | Upcoming Order Notification | Fires when an upcoming order notification is sent based on configured buffer days | ### Schedule changes | Trigger | Description | | ------------------------------------- | -------------------------------------------------- | | Subscription Billing Interval Changed | Fires when the billing frequency is changed | | Subscription Next Order Date Changed | Fires when the next billing/order date is modified | ## Trigger properties ### Subscription properties Included with every trigger: | Property | Type | Description | | ------------------------- | ------ | -------------------------------------------------------------------------- | | `Subscription ID` | Number | Internal subscription contract ID | | `GraphQL Subscription ID` | String | GraphQL ID of the subscription contract | | `Status` | String | `ACTIVE`, `PAUSED`, `CANCELLED`, `EXPIRED`, or `FAILED` | | `Next Billing Date` | String | Next billing date in UTC ISO 8601. Empty string if not `ACTIVE`. | | `Billing Interval` | String | Billing frequency unit: `DAY`, `WEEK`, `MONTH`, `YEAR` | | `Billing Interval Count` | Number | Number of intervals between billings (e.g. `2` + `MONTH` = every 2 months) | | `Delivery Interval` | String | Delivery frequency unit: `DAY`, `WEEK`, `MONTH`, `YEAR` | | `Delivery Interval Count` | Number | Number of intervals between deliveries | | `Original Order ID` | Number | ID of the first order. `0` for imported contracts. | | `Original Order Name` | String | Name of the first order. Empty string for imported contracts. | | `Cancel Reason` | String | Reason for cancellation (empty string if not cancelled) | | `Pause Reason` | String | Reason for pause (empty string if not paused) | | `Total Successful Orders` | Number | Total successfully completed orders for this contract | | `Line Items` | Array | Array of line items (see schema below) | | `Order Note Attributes` | Array | Array of key-value note attributes | ### Customer properties Included with every trigger: | Property | Type | Description | | ----------------------- | --------- | ------------------------------------------------------- | | `customer_reference` | Reference | Shopify customer reference | | `Customer Email` | Email | Customer's email address | | `Customer Phone` | String | Customer's phone number (empty string if not available) | | `Customer Display Name` | String | Customer's display name | | `Customer First Name` | String | Customer's first name | | `Customer Last Name` | String | Customer's last name | ### Billing event properties For **Subscription Billing Success**, **Subscription Billing Failure**, and **Upcoming Order Notification** only: | Property | Type | Description | | ------------------------ | ------ | ----------------------------------------------- | | `Billing Attempt ID` | Number | ID of the billing attempt | | `Billing Attempt Status` | String | `SUCCESS`, `FAILURE`, `PENDING`, or `SCHEDULED` | | `Billing Attempt Count` | Number | Number of billing attempts | | `Billing Date` | String | Scheduled billing date (UTC ISO 8601) | | `Billing Attempt Time` | String | Actual attempt timestamp (UTC ISO 8601) | For **Subscription Billing Success** only: | Property | Type | Description | | ---------------------- | --------- | ---------------------------------- | | `Recurring Order ID` | Number | Order ID from successful renewal | | `Recurring Order Name` | String | Order name from successful renewal | | `order_reference` | Reference | Shopify order reference | ### Subscription Updated properties For the **Subscription Updated** trigger only: | Property | Type | Description | | --------------------- | ------ | ------------------------------------------- | | `Contract Amount` | Number | Total line item amount in contract currency | | `Contract Amount USD` | Number | Total line item amount converted to USD | ### Line items schema Each object in the `Line Items` array contains: | Field | Type | Description | | ---------------------- | ------ | -------------------------------------------- | | `lineItemId` | String | ID of the line item | | `variantId` | Float | Shopify variant ID | | `graphqlVariantId` | String | GraphQL variant ID | | `productId` | ID | Shopify product ID (GraphQL format) | | `quantity` | Int | Quantity | | `graphqlSellingPlanId` | String | Selling plan ID for this line | | `sellingPlanName` | String | Selling plan name | | `customAttributes` | Array | Key-value custom attributes on the line item | ## Actions All actions return at minimum a `success_message` output field. Every action is logged in the subscription activity log with source `SHOPIFY_FLOW`. ### Line item management Add a product variant to an existing subscription. **Input fields:** | Field | Type | Required | Description | | --------------------- | ------ | -------- | ---------------------------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `product_variant_id` | String | Yes | Shopify product variant ID to add | | `quantity` | String | Yes | Quantity to add | | `is_one_time_product` | String | | `"true"` to add as a non-recurring line item. Default: `"false"` | **Output fields:** `contract_id`, `product_variant_id`, `quantity`, `is_one_time_product`, `success_message` Remove a product from a subscription. **Input fields:** | Field | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `line_id` | String | Yes | Line item ID to remove | | `remove_discount` | String | | Whether to also remove the associated discount | **Output fields:** `contract_id`, `line_id`, `remove_discount`, `success_message` Change the quantity of a product in a subscription. **Input fields:** | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------ | | `contract_id` | String | Yes | Subscription contract ID | | `line_id` | String | Yes | Line item ID | | `quantity` | String | Yes | New quantity | **Output fields:** `contract_id`, `line_id`, `quantity`, `success_message` Change the price of a product in a subscription. **Input fields:** | Field | Type | Required | Description | | ----------------------- | ------- | -------- | ---------------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `line_id` | String | Yes | Line item ID | | `base_price` | String | Yes | New base price | | `remove_pricing_policy` | Boolean | | Remove the existing pricing policy. Default: `false` | **Output fields:** `contract_id`, `line_id`, `base_price`, `remove_pricing_policy`, `success_message` Update custom attributes on a line item. **Input fields:** | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `line_id` | String | Yes | Line item ID | | `attributes` | String | Yes | JSON array of key-value pairs | **Output fields:** `contract_id`, `line_id`, `success_message` Swap a product variant for another in a subscription. **Input fields:** | Field | Type | Required | Description | | ------------------------ | ------ | -------- | ----------------------------------------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `old_variant_id` | String | Yes | Comma-separated variant IDs to replace | | `new_variant_id` | String | Yes | Comma-separated variant IDs with optional quantities (e.g. `11111:2,22222:1`) | | `old_line_id` | String | | Specific line ID when multiple lines share the same variant | | `carry_forward_discount` | String | | Whether to carry the existing discount to the new variant | **Output fields:** `contract_id`, `old_variant_id`, `new_variant_id`, `success_message` ### Subscription management Set a subscription's status to Active, Paused, or Cancelled. **Input fields:** | Field | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `status` | String | Yes | `ACTIVE`, `PAUSED`, or `CANCELLED` | **Output fields:** `contract_id`, `status`, `success_message` Hide a subscription from the customer portal. **Input fields:** `contract_id` (required) **Output fields:** `contract_id`, `success_message` Combine two subscriptions into one. **Input fields:** | Field | Type | Required | Description | | ------------------------- | ------ | -------- | ------------------------- | | `source_contract_id` | String | Yes | Contract ID to merge from | | `destination_contract_id` | String | Yes | Contract ID to merge into | **Output fields:** `source_contract_id`, `destination_contract_id`, `success_message` Split specific line items out into a new subscription contract. **Input fields:** | Field | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------ | | `contract_id` | String | Yes | Subscription contract ID | | `line_ids` | String | Yes | Comma-separated line item IDs to move | | `attempt_billing` | String | | Whether to immediately bill the new contract after splitting | **Output fields:** `original_contract_id`, `new_contract_id`, `line_ids_moved`, `success_message` **Update Order Note** — sets the order note text on a subscription. Required: `contract_id`, `order_note`. **Update Note Attributes** — sets custom key-value note attributes. Required: `contract_id`, `note_attributes` (JSON array). Optional: `overwrite_existing` (boolean, default `false`). Output includes `attributes_count`. ### Billing & schedule Change the billing frequency. **Input fields:** | Field | Type | Required | Description | | ------------------------ | ------ | -------- | ------------------------------------ | | `contract_id` | String | Yes | Subscription contract ID | | `billing_interval_count` | String | Yes | Number of intervals between billings | | `billing_interval` | String | Yes | `DAY`, `WEEK`, `MONTH`, or `YEAR` | **Output fields:** `contract_id`, `billing_interval_count`, `billing_interval`, `success_message` Change billing and delivery frequency using an existing selling plan. **Input fields:** `contract_id` (required), `selling_plan_id` (required) **Output fields:** `contract_id`, `selling_plan_id`, `selling_plan_name`, `billing_interval_count`, `billing_interval`, `delivery_interval_count`, `delivery_interval`, `success_message` Reschedule the next billing date. Dates must use ISO 8601 format: `2026-03-15T10:00:00Z`. **Input fields:** `contract_id` (required), `next_billing_date` (required) **Output fields:** `contract_id`, `next_billing_date`, `success_message` Skip (or unskip) the next scheduled order. **Input fields:** | Field | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `line_id` | String | Yes | Line item ID | | `is_skip` | Boolean | | `true` to skip, `false` to unskip. Default: `true` | **Output fields:** `contract_id`, `line_id`, `is_skip`, `success_message` Trigger immediate billing. Requires either `contract_id` or `billing_attempt_id`. The Attempt Billing action requires additional permission. Contact [support@appstle.com](mailto:support@appstle.com) to enable it for your store. **Output fields:** `contract_id`, `success_message` **Update Max Cycles** — set the maximum billing cycles before auto-cancellation. Required: `contract_id`, `max_cycles`. **Update Min Cycles** — set the minimum billing cycles before the customer can cancel. Required: `contract_id`, `min_cycles`. ### Discounts & pricing Apply a discount code to a subscription. **Input fields:** `contract_id` (required), `discount_code` (required) **Output fields:** `contract_id`, `discount_code`, `success_message` Remove a discount. Provide either `discount_id` or `discount_code`. **Input fields:** `contract_id` (required), `discount_id` or `discount_code` (one required) **Output fields:** `contract_id`, `discount_id`, `success_message` Update pricing policy with cycle-based discounts. **Input fields:** | Field | Type | Required | Description | | -------------------- | ------- | -------- | -------------------------------------------------------- | | `contract_id` | String | Yes | Subscription contract ID | | `base_price` | String | Yes | Base price for the line item | | `line_id` | String | | Specific line item ID — if omitted, applies to all lines | | `cycles` | String | | JSON defining cycle-based pricing adjustments | | `overwrite_existing` | Boolean | | Replace existing pricing policy. Default: `false` | **Output fields:** `contract_id`, `line_id`, `base_price`, `success_message` ### Delivery & shipping Change the delivery frequency. **Input fields:** `contract_id` (required), `delivery_interval_count` (required), `delivery_interval` (required — `DAY`, `WEEK`, `MONTH`, or `YEAR`) Change the shipping method on a subscription. **Input fields:** `contract_id` (required), `delivery_method_title` (required). Optional: `delivery_method_code`, `delivery_method_presentment_title`, `delivery_method_id`. Change the shipping price. Required: `contract_id`, `delivery_price`. Update the delivery address. **Required fields:** `contract_id`, `address1`, `city`, `country_code` **Optional fields:** `address2`, `province`, `province_code`, `zip`, `country`, `first_name`, `last_name`, `phone`, `company` Send a payment method update request to the customer. Required: `contract_id`. ## Example workflows **Trigger:** Subscription Billing Failure **Condition:** Billing Attempt Count ≥ 3 **Action:** Update Subscription Status → `PAUSED` **Trigger:** Subscription Billing Success **Condition:** Total Successful Orders ≥ 6 **Action:** Apply Discount Code → `LOYAL10` **Trigger:** Subscription Cancelled **Condition:** Contract Amount > 100 **Action:** Send Slack notification (via Shopify Flow's Slack connector) **Trigger:** Scheduled (monthly, via Shopify Flow's scheduler) **Action:** Replace Variant → swap old variant ID for new variant ID ## Field reference **Attributes JSON format** — for `attributes` and `note_attributes` fields: ```json theme={null} [ {"key": "gift_message", "value": "Happy Birthday!"}, {"key": "delivery_instructions", "value": "Leave at door"} ] ``` **Replace Variant format:** * `old_variant_id`: comma-separated IDs to replace — `12345,67890` * `new_variant_id`: comma-separated IDs with optional quantities — `11111:2,22222:1` # Appstle Subscriptions webhook events and setup Source: https://developers.appstle.com/subscription/webhooks Configure webhook endpoints to receive real-time subscription events, verify Svix signatures, handle retries, and troubleshoot delivery failures. Webhooks let you receive real-time HTTP notifications when subscription events happen in your Appstle account. Instead of polling the API, you register an endpoint URL and Appstle sends an HTTP POST request to it whenever an event occurs. Appstle webhooks are powered by [Svix](https://www.svix.com/), which provides automatic retries with exponential backoff, cryptographic signature verification, and detailed delivery logs. ## Getting started Log in to your Appstle dashboard, go to **Settings → Webhooks**, add your webhook endpoint URL, and select which events you want to receive. Your endpoint must return a `2xx` status code (e.g. `200 OK`) to acknowledge receipt. Process the event asynchronously in a background job — slow responses will time out and trigger retries. Every webhook request includes Svix signature headers. Always verify the signature before processing to ensure the request is authentic. See [Signature verification](#signature-verification) below. ## Event types | Event type | Description | Payload type | | ------------------------------------------ | ------------------------------ | --------------------- | | `subscription.created` | New subscription created | Subscription contract | | `subscription.updated` | Subscription details updated | Subscription contract | | `subscription.activated` | Subscription activated | Subscription contract | | `subscription.paused` | Subscription paused | Subscription contract | | `subscription.cancelled` | Subscription cancelled | Subscription contract | | `subscription.next-order-date-changed` | Next order date modified | Subscription contract | | `subscription.billing-interval-changed` | Billing frequency changed | Subscription contract | | `subscription.billing-success` | Payment processed successfully | Billing attempt | | `subscription.billing-failure` | Payment failed | Billing attempt | | `subscription.billing-skipped` | Billing cycle skipped | Billing attempt | | `subscription.upcoming-order-notification` | Upcoming order reminder sent | Billing attempt | ## Payload structure All webhooks use this envelope: ```json theme={null} { "type": "subscription.created", "data": { // Event-specific payload } } ``` ## Subscription contract payloads The following events carry a full subscription contract in `data`: `subscription.created`, `subscription.updated`, `subscription.activated`, `subscription.paused`, `subscription.cancelled`, `subscription.next-order-date-changed`, `subscription.billing-interval-changed` ```json theme={null} { "type": "subscription.created", "data": { "id": "gid://shopify/SubscriptionContract/12345", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z", "nextBillingDate": "2026-02-15", "status": "ACTIVE", "deliveryPrice": { "amount": "5.00", "currencyCode": "USD" }, "lastPaymentStatus": "SUCCEEDED", "billingPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [{ "type": "MONTHDAY", "day": 15, "month": null, "cutoffDay": null }], "maxCycles": null, "minCycles": null }, "deliveryPolicy": { "interval": "MONTH", "intervalCount": 1, "anchors": [{ "type": "MONTHDAY", "day": 15, "month": null, "cutoffDay": null }] }, "lines": { "nodes": [ { "id": "gid://shopify/SubscriptionLine/67890", "productId": "gid://shopify/Product/11111", "variantId": "gid://shopify/ProductVariant/22222", "sellingPlanId": "gid://shopify/SellingPlan/33333", "sellingPlanName": "Subscribe & Save 10%", "title": "Premium Coffee Beans", "variantTitle": "1lb / Medium Roast", "sku": "COFFEE-1LB-MED", "quantity": 2, "taxable": true, "currentPrice": { "amount": "27.00", "currencyCode": "USD" }, "lineDiscountedPrice": { "amount": "24.30", "currencyCode": "USD" }, "pricingPolicy": { "basePrice": { "amount": "30.00", "currencyCode": "USD" }, "cycleDiscounts": [ { "afterCycle": 0, "adjustmentType": "PERCENTAGE", "adjustmentValue": { "percentage": 10.0 }, "computedPrice": { "amount": "27.00", "currencyCode": "USD" } } ] } } ] }, "customer": { "id": "gid://shopify/Customer/55555", "email": "customer@example.com", "displayName": "John Doe", "firstName": "John", "lastName": "Doe", "phone": "+1-555-123-4567" }, "customerPaymentMethod": { "id": "gid://shopify/CustomerPaymentMethod/66666", "instrument": { "__typename": "CustomerCreditCard", "brand": "VISA", "expiresSoon": false, "expiryMonth": 12, "expiryYear": 2028, "lastDigits": "4242", "maskedNumber": "•••• •••• •••• 4242", "name": "John Doe" } }, "deliveryMethod": { "__typename": "SubscriptionDeliveryMethodShipping", "address": { "firstName": "John", "lastName": "Doe", "address1": "123 Main Street", "city": "San Francisco", "province": "California", "country": "United States", "zip": "94102" }, "shippingOption": { "title": "Standard Shipping", "code": "STANDARD" } } } } ``` ### Subscription contract fields | Field | Type | Description | | ----------------------------- | -------- | ------------------------------------------------------- | | `id` | String | Shopify GraphQL ID of the subscription contract | | `status` | Enum | `ACTIVE`, `PAUSED`, `CANCELLED`, `EXPIRED`, or `FAILED` | | `nextBillingDate` | Date | ISO 8601 date of next billing (`YYYY-MM-DD`) | | `createdAt` | DateTime | ISO 8601 timestamp when subscription was created | | `updatedAt` | DateTime | ISO 8601 timestamp of last update | | `lastPaymentStatus` | Enum | `SUCCEEDED`, `FAILED`, or null | | `billingPolicy.interval` | Enum | `DAY`, `WEEK`, `MONTH`, or `YEAR` | | `billingPolicy.intervalCount` | Integer | Number of intervals between billings | | `billingPolicy.maxCycles` | Integer | Maximum billing cycles (null = unlimited) | | `billingPolicy.minCycles` | Integer | Minimum billing cycles before cancellation is allowed | | `lines.nodes` | Array | Subscription line items | | `customer` | Object | Customer details | | `customerPaymentMethod` | Object | Payment method (credit card, PayPal, or Shop Pay) | | `deliveryMethod` | Object | Delivery method (shipping, local delivery, or pickup) | | `discounts.nodes` | Array | Applied discounts | | `note` | String | Internal subscription note | | `customAttributes` | Array | Custom key-value attributes | ## Billing attempt payloads The following events carry billing attempt data: `subscription.billing-success`, `subscription.billing-failure`, `subscription.billing-skipped`, `subscription.upcoming-order-notification` ```json theme={null} { "type": "subscription.billing-success", "data": { "id": 98765, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99999", "contractId": 12345, "status": "SUCCESS", "billingDate": "2026-02-15T00:00:00Z", "attemptTime": "2026-02-15T10:30:00Z", "attemptCount": 1, "graphOrderId": "gid://shopify/Order/77777", "orderId": 77777, "orderName": "#1002", "orderAmount": 59.30, "retryingNeeded": false, "upcomingOrderEmailSentStatus": "SENT" } } ``` ```json theme={null} { "type": "subscription.billing-failure", "data": { "id": 98766, "shop": "example-store.myshopify.com", "billingAttemptId": "gid://shopify/SubscriptionBillingAttempt/99998", "contractId": 12345, "status": "FAILURE", "billingDate": "2026-03-15T00:00:00Z", "attemptTime": "2026-03-15T10:30:00Z", "attemptCount": 1, "graphOrderId": null, "orderId": null, "retryingNeeded": true, "transactionFailedEmailSentStatus": "SENT", "billingAttemptResponseMessage": "INVALID_PAYMENT_METHOD: The payment method is invalid. Please update your payment information." } } ``` Common `billingAttemptResponseMessage` values: `INVALID_PAYMENT_METHOD`, `INSUFFICIENT_FUNDS`, `AUTHENTICATION_REQUIRED`, `CARD_DECLINED`, `EXPIRED_PAYMENT_METHOD`, `INVENTORY_ALLOCATIONS_NOT_FOUND`, `PAYMENT_METHOD_VERIFICATION_FAILED`. ```json theme={null} { "type": "subscription.billing-skipped", "data": { "id": 98767, "shop": "example-store.myshopify.com", "contractId": 12345, "status": "SKIPPED", "billingDate": "2026-04-15T00:00:00Z", "retryingNeeded": false, "inventorySkippedAttemptCount": 1, "inventorySkippedRetryingNeeded": true, "partialLinesSkipped": "OUT_OF_STOCK", "billingAttemptResponseMessage": "INVENTORY_ALLOCATIONS_NOT_FOUND: One or more products are out of stock." } } ``` ```json theme={null} { "type": "subscription.upcoming-order-notification", "data": { "id": 98768, "shop": "example-store.myshopify.com", "contractId": 12345, "status": "PENDING", "billingDate": "2026-05-15T00:00:00Z", "attemptCount": 0, "retryingNeeded": false, "upcomingOrderEmailSentStatus": "SENT", "upcomingOrderSmsSentStatus": "SENT" } } ``` ### Billing attempt fields | Field | Type | Description | | ------------------------------- | -------- | ---------------------------------------------------------------------- | | `id` | Long | Unique identifier for this billing attempt record | | `shop` | String | Shopify store domain | | `billingAttemptId` | String | Shopify GraphQL billing attempt ID | | `contractId` | Long | Related subscription contract ID (numeric, no `gid://` prefix) | | `status` | Enum | `SUCCESS`, `FAILURE`, `SKIPPED`, or `PENDING` | | `billingDate` | DateTime | ISO 8601 scheduled billing date | | `attemptTime` | DateTime | ISO 8601 actual attempt timestamp | | `attemptCount` | Integer | Number of billing attempts for this cycle | | `graphOrderId` | String | Shopify GraphQL order ID (if successful) | | `orderId` | Long | Shopify numeric order ID (if successful) | | `orderName` | String | Order name like `#1234` (if successful) | | `orderAmount` | Double | Order total in shop currency | | `retryingNeeded` | Boolean | Whether automatic retry is scheduled | | `billingAttemptResponseMessage` | String | Error message if failed/skipped, null if successful | | `inventorySkippedAttemptCount` | Integer | Count of skips due to inventory issues | | `partialLinesSkipped` | Enum | Reason for partial line skipping: `OUT_OF_STOCK`, `PRICE_CHANGE`, etc. | ## Signature verification Every webhook request is signed by Svix. You must verify the signature before processing to ensure the request is authentic and has not been tampered with. Svix adds three headers to every request: | Header | Description | | ---------------- | -------------------------------------------- | | `svix-id` | Unique message ID — use this for idempotency | | `svix-timestamp` | Unix timestamp when the message was sent | | `svix-signature` | Cryptographic signature | Find your webhook signing secret in **Settings → Webhooks** in the Appstle dashboard. ```javascript Node.js theme={null} const { Webhook } = require('svix'); const secret = 'whsec_your_webhook_signing_secret'; app.post('/webhooks/appstle', (req, res) => { const payload = JSON.stringify(req.body); const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }; const wh = new Webhook(secret); let event; try { event = wh.verify(payload, headers); } catch (err) { return res.status(400).send('Webhook signature verification failed'); } console.log('Event type:', event.type); console.log('Event data:', event.data); res.status(200).send('OK'); }); ``` ```python Python theme={null} from svix.webhooks import Webhook secret = "whsec_your_webhook_signing_secret" @app.route("/webhooks/appstle", methods=["POST"]) def webhook(): payload = request.get_data() headers = { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), } wh = Webhook(secret) try: event = wh.verify(payload, headers) except Exception: return "Webhook signature verification failed", 400 print("Event type:", event["type"]) return "OK", 200 ``` Svix also provides verification libraries for Go, Ruby, PHP, Java, and C#. See the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how) for all language examples. ## Testing webhooks ### Using Svix Play 1. Go to **Settings → Webhooks** in your Appstle dashboard. 2. Click on your endpoint. 3. Use the **Send Example** feature to send a sample event. 4. Verify your endpoint receives and processes the webhook correctly. ### Local development Use [ngrok](https://ngrok.com/) or [localtunnel](https://localtunnel.me/) to expose your local server: ```bash theme={null} ngrok http 3000 ``` Then add the generated public URL (e.g. `https://abc123.ngrok.io/webhooks/appstle`) as a webhook endpoint in your Appstle dashboard. ## Retry schedule If your endpoint does not return a `2xx` status code, Svix retries automatically using exponential backoff over 5 attempts across 3 days. Endpoints that fail consistently are automatically disabled to prevent wasted resources. You can manually retry failed deliveries from the Svix dashboard, and view all delivery attempts in **Settings → Webhooks → Message Logs**. ## Troubleshooting | Problem | Solution | | --------------------------------- | --------------------------------------------------------------------------------------------------------- | | Signature verification fails | Use the exact signing secret from your dashboard. Do not modify the raw request body before verification. | | Timeouts | Return `200 OK` immediately and process the event in a background job. | | Wrong status codes | Return `2xx` for all successful receipts, even if you have business logic errors. | | CSRF protection blocking webhooks | Exempt your webhook endpoint from CSRF checks in your web framework. | | Duplicate events | Webhooks may be delivered more than once. Use the `svix-id` header to make your processing idempotent. | Log the raw webhook payload during development so you can inspect the exact data structure. Test with a simple endpoint that just logs and returns `200 OK` before adding business logic. Need help? Email [support@appstle.com](mailto:support@appstle.com) with your endpoint URL and the `svix-id` of any failing message.