Skip to content

Shopify App Pricing (managed recurring plans)

AppiFire uses Shopify App Pricing for recurring plans configured in the Partner Dashboard. One-time AI credit packs still use the Billing API (appPurchaseOneTimeCreate) — no Partner Dashboard packages are required for credit packs.

Related: Credits wallet · Billing & usage · Shopify billing & payments


Plan handlePriceInternal shops.plan
free$0/mofree
appifire-ai-chat-plan-1$20/mopaid

Constants: SHOPIFY_PLAN_HANDLES in app/lib/plans.server.js. Mapping: mapPlanHandleToInternal(planHandle).

Do not create new recurring charges with appSubscriptionCreate after opting into Shopify App Pricing — Shopify blocks or duplicates billing.


  • OAuth / afterAuth creates the shop with plan: "free".
  • No forced plan picker on install.
  • Merchant can use the app on Free until they choose to upgrade.
  • Upgrade and Change plan on Billing use window.top.location.href to open Shopify’s plan picker (embedded apps cannot load admin.shopify.com inside the iframe — a plain redirect() inside the iframe shows “refused to connect”).

    https://admin.shopify.com/store/{storeHandle}/charges/{appHandle}/pricing_plans

    Implemented in app/routes/app.billing.jsx (client-side top navigation, not iframe redirect).

  • SHOPIFY_APP_HANDLE (default appifire-ai-chat) and store handle from *.myshopify.com — see app/lib/billing-plans.server.js.

  • Downgrade: merchant picks Free on the same page (not a cancel-only path in the app UI).


Shopify redirects back with query params (e.g. plan_handle=free or plan_handle=appifire-ai-chat-plan-1).

RouteRole
app/routes/app.billing.confirm.jsxSync plan when plan_handle or legacy charge_id is present → redirect /app/billing?plan_selected=1
app/routes/app.billing.jsx loaderAlso reads plan_handle on load; runs resolveSubscriptionState + syncShopPlanFromSubscriptionState

Configure welcome / redirect URLs per plan in Partner Dashboard to hit /app/billing/confirm or /app/billing.


Wrong (pre-migration):

effectivePlan = activeSubscriptions.length > 0 ? "paid" : "free";

With managed pricing, Free can appear as an active $0 subscription. That would incorrectly show buy-credits UI and skip free reply caps.

Correct: Paid only when subscription maps to appifire-ai-chat-plan-1 (or recurring line-item price > $0), via:

  1. URL plan_handle (highest priority when present)
  2. Partner API activeSubscription(appId, shopId) when SHOPIFY_PARTNER_* env vars are set (app/lib/partner-api.server.js)
  3. Admin API activeSubscriptions + classifyAdminSubscription() (app/lib/billing-plans.server.js)

Webhook app_subscriptions/update uses classifySubscriptionByNameAndPrice() so active $0 subs do not set plan = paid.


StepDetail
GatemerchantHasPaidPlan()paid plan only, not “any active subscription”
CreatePOST /app/billing/buy-creditsappPurchaseOneTimeCreate (dynamic $10–$200 from ADD_ON_CREDIT_AMOUNTS)
Confirm/app/billing/buy-credits/confirm?charge_id=…
Balanceshops.credit_balance (USD decimal)

Terminal window
SHOPIFY_APP_HANDLE=appifire-ai-chat
# Optional Partner API (recommended before legacy Billing API cutoff ~April 2026):
SHOPIFY_PARTNER_ORG_ID=
SHOPIFY_PARTNER_CLIENT_ID=
SHOPIFY_PARTNER_CLIENT_SECRET=
SHOPIFY_PARTNER_APP_ID=
# Optional: gid://shopify/Shop/... if Partner shop lookup fails
SHOPIFY_PARTNER_SHOP_GID=

Also: PAID_PLAN_MONTHLY_PRICE, FREE_PLAN_REPLIES_CAP, PAID_PLAN_INCLUDED_CREDITS_USD — see .env.sample.


ConcernFile
Plan handlesapp/lib/plans.server.js
Plan picker URL, resolve/syncapp/lib/billing-plans.server.js
Partner API (optional)app/lib/partner-api.server.js
Billing UI + loaderapp/routes/app.billing.jsx
Post-picker confirmapp/routes/app.billing.confirm.jsx
Credit packsapp/routes/app.billing.buy-credits.jsx, app.billing.buy-credits.confirm.jsx
Subscription webhookapp/routes/webhooks.app_subscriptions.update.jsx

ScenarioExpected
Fresh installshops.plan = free, no charge
UpgradePlan picker → $20 plan → plan = paid, included credits granted
DowngradePlan picker → Free → plan = free, free reply cap
Free + $0 managed subNot paid; no buy-credits
Buy credits (paid)One-time charge → balance increases
Buy credits (free)403 with clear message
Cancel / lapseLeftover balance usable; suppress_included_credits_after_subscription_lapse