docs
/
AI skills

Subscription plans

Sell a membership or a product as a subscription. A plan with its price, billing period and trial, the product page that sells it, what the customer sees and can do in their account, and how renewals, pauses, plan changes, cancellations and failed payments really run. Free plans work with nothing connected; paid plans need the owner's Stripe.

A plan is the price and the billing period: $20 every month, $30 every 3 months with a 7-day trial, or free. A subscription is one customer on one plan. Appmint runs every subscription itself: its status, its dates, its renewals, its retries and the emails about them. A payment provider only keeps the customer's card and charges it when Appmint asks.

Only Stripe can keep a card and charge it later. Without Stripe, a business can still run free plans and memberships paid at the counter (cash or check, recorded by staff), but no shopper can start a paid plan online.

A subscription is not sold through the cart. A product linked to a plan shows a Subscribe button on the built-in store page. If page code adds that product to the cart instead, checkout sells it once at the product's own price and creates no subscription. Sell plans with Subscribe, never with cart.add.

Ask first

  • The plans. For each: the name customers see, the price, how often it bills (every 1 month, every 3 months, every year, once for life), and the trial in days, if any.
  • Free or paid? A paid plan needs the owner's Stripe connected first (Take payments on the site). Ask whether it is.
  • What the plan includes, as short lines (Newsletter: monthly). They show on the store page.
  • Sold on the website, or set up by staff? On the website, each plan needs a product to sell it from (step 3).
  • Cash or check members? Staff can start those (step 4), and staff record each renewal payment by hand.

Steps

1. Finance › Gateway: check what can charge a plan

call_appengine GET storefront/subscriptions/providers. Every payment provider comes back with configured (this business has it set up) and recurring (it can keep a card and charge it later). Only stripe is ever recurring: true. The others carry a reason, such as "PayPal here takes one-off orders only…".

What is set upWhat can be sold online
Stripe configured: trueFree plans and paid plans, trials included
No StripeFree plans only. Paid plans only at the counter, by staff (step 4)

Stop: connecting Stripe means the owner enters secret keys. Follow Take payments on the site. The AI never asks for keys.

Hands on: whether paid plans can be sold online. Check: the stripe row says configured: true, recurring: true, or you have told the person that only free and counter plans will work.

2. Storefront › Subscriptions: the plan

Create one sf_subscription_plan per plan, the same record the plan drawer saves (repo_create, or Storefront › Subscriptions › Plans & Pricing › New plan at /subscription?tab=plans).

FieldValue
nameThe plan id: 3–50 letters, numbers, - or _ (gold-monthly). Unique. Subscriptions and products refer to the plan by this name, so it never changes after saving
titleWhat customers see (Gold membership)
priceThe price per period. 0 is a free plan
displayPriceOptional "was" price, shown struck through
duration, durationUnitThe period: 1 + month. Units are minute, hour, day, week, month, year, forever (one payment, never renews)
trialDays free before the first charge. Paid plans only; ignored on a free plan
statusactive. An inactive plan is hidden from shoppers
features[{ name, value, description }]: the lines on the store page
descriptionWhat the subscriber gets
upstream.namestripe for a paid plan. Leave it out when only one provider can charge; it is needed when there are several

A month is a calendar month: a plan started on 31 January renews on 28 or 29 February.

currency is read from the plan when it is set, and is USD otherwise. addOns is saved but nothing uses it.

Hands on: each plan's name. Check: call_appengine GET storefront/subscriptions/plans (public, what shops show). Every active plan is listed, cheapest first, with price, duration, durationUnit, trial and features. Open the plan at /subscription?tab=plans&planId=<sk>.

3. Storefront › Products: the product that sells it (website only)

Set the product's plan to the plan's name, in the product editor (/product?openId=<sk>, Grouping › Subscription Plan) or with repo_update on data.plan. Use a new product for the plan, not a product that is also sold one at a time.

On the built-in store (/store/<slug>), a product with a plan shows the plan's price per period and a Subscribe button instead of Add to cart. Product cards hide Add to cart for it. The plan's price is shown, never the product's price, and price lists or group prices never change it.

Subscribe asks a guest to sign in first. Then:

  • Free plan: the subscription is active at once, and the shopper lands on /account/subscriptions.
  • Paid plan: the shopper goes to Stripe's own card page. When they come back, the card is saved and the first period is charged (or the trial starts).

On a page of your own, the same calls are on the page runtime, and every one needs a signed-in shopper:

CallWhat it does
appmint.storefront.subscribe({ plan, successUrl, cancelUrl })Subscribes the shopper (productId in place of plan uses the product's plan). A paid plan answers with setup.redirectUrl (Stripe's card page); send them there
appmint.storefront.subscriptions()Their subscriptions
appmint.storefront.subscription(id)One, with allowedActions
appmint.storefront.updateSubscription({ id, action, … })Any action in step 5

Hands on: the product's slug. Check: open /store/<slug> on the site. It shows the plan's price, / month, and SUBSCRIBE.

4. Storefront › Subscriptions: subscribe a customer (staff)

Staff subscribe someone with call_appengine POST storefront/subscriptions/subscribe and { plan, email }, the same call as Create Subscription (/subscription?tab=create).

BodyResult
A free planactive now; the period runs from today
A paid plan, no cardStripe set up: new, awaiting a card; the customer adds it from their account. No Stripe: refused, SUBSCRIPTION_NO_PROVIDER, and nothing is saved
A paid plan with a trialStripe set up: trialing; the first charge is on the trial's last day. No Stripe: refused the same way
transaction: { gateway: "cash" | "check", ref }A counter membership: active now at the plan's price, no trial. Each renewal then waits as past_due, "Payment due at the counter (cash or check)…", until staff use Record payment
trial: falseSkips the plan's trial
autoRenew: falseEnds at the end of the first period
noteGoes into its history

The gateways that count as counter payments are cash, check, cheque, offline, counter and manual. Only staff can start one. A shopper who tries is refused with "A payment taken outside the card gateway (cash, cheque, bank transfer) is recorded by the business, not online." (403 SUBSCRIPTION_COUNTER_STAFF_ONLY).

A payment named in the request is checked before it counts. A transaction: { gateway: "stripe", ref } counts only when Stripe confirms that payment succeeded, it isn't already recorded on another sale, and its amount covers what is due (else 402 PAYMENT_NOT_CONFIRMED or SUBSCRIPTION_PAYMENT_SHORT, or 409 PAYMENT_ALREADY_USED). Any other gateway (cash, check, bank transfer) is staff only. Every subscription call needs a signed-in shopper, acting on their own subscriptions, or staff; with nobody signed in it answers 401 "Sign in to subscribe." or "Sign in to manage your subscription." (storefront.subscription.service.ts withCheckedPayment).

Stop: subscribing emails the customer a confirmation, and a counter membership records money taken. Show the plan, the customer and how they paid, and wait for yes.

Hands on: the subscription's number (data.name, e.g. FCT3VL6B3) and its id. Check: open /subscription?openId=<number or id>. Its status, period and history are right.

5. Storefront › Subscriptions: run one

Each action is call_appengine POST storefront/subscriptions/<id or number>/<action>. Each one answers with the subscription plus allowedActions (what it accepts next), nextBoundary (when the daily run next acts on it), nextChargeDate and paymentMethodSaved. Read allowedActions first: anything not on it is refused with 409 SUBSCRIPTION_ACTION_NOT_ALLOWED.

StatusAllowed actions
new (waiting for the first payment)change-plan, cancel
trialing, activepause, change-plan, renew, cancel
trialing, active, set to end at period endreactivate, change-plan, cancel
past_duerenew (retries the payment), change-plan, cancel
pausedresume, cancel
cancelled, expiredreactivate

A paid subscription that is still running also allows payment-method and record-payment. Any subscription with a payment left to give back allows refund.

ActionBodyWhat happens
pause{ resumeAt?, reason? }Paused. Nothing renews while paused. resumeAt must be in the future; the daily run resumes it that day. Without it, it stays paused until resumed
resume{}Back to active (or trialing). The time spent paused is added to the current period
change-plan{ plan, when: "period_end" }Moves at the next renewal, no charge. The same call with the current plan withdraws it
change-plan{ plan, when: "now" } (the default)During a paid period, the end date stays and the customer pays the difference for the unused part, if it is more. A cheaper plan refunds nothing. From a free, new or past-due subscription, a new period starts now at the full new price. In a trial, only the plan changes
renew{}Free: starts the next period now (early renewal adds a period after the current one). Paid: charges the saved card. Past due: retries the payment
cancel{ when?: "now" | "period_end", reason? }Default while a period runs: ends at period end, no more charges. now ends it at once. It never refunds by itself
reactivate{}Set to end: the cancellation is withdrawn. Cancelled or expired: a new period starts now. A paid plan charges the saved card, or waits as new for one
payment-method{ successUrl, cancelUrl }Answers setup.redirectUrl, Stripe's card page. Coming back with { checkoutSessionId } saves the card. A past-due subscription is charged again at once; a new one is charged its first period
record-payment (staff){ gateway, ref, amount? }A payment taken outside Stripe: activates a new one, ends a trial, clears past due, or pays the next period. It is written as a transaction
refund (staff){ amount?, reason?, cashReturned? }Gives back the last payment, or part of it. A Stripe payment goes back through Stripe. A payment taken by hand is only recorded, and needs cashReturned: true

Errors to expect: SUBSCRIPTION_CARD_REQUIRED (402, moving to a paid plan with no card), SUBSCRIPTION_SAME_PLAN (409), SUBSCRIPTION_ALREADY_ENDING (409), SUBSCRIPTION_BAD_RESUME_DATE (400), SUBSCRIPTION_NOTHING_TO_REFUND (400).

Studio shows the same actions on the subscription's screen: Retry payment, Renew now (or Start next period now on a free plan), Resume, Reactivate, Change plan, Pause, Cancel, Record payment, Refund payment. A shopper may act only on their own subscriptions; anyone else's answers 404.

Stop: every action emails the customer (see What the customer sees). Pause, cancel, change-plan, renew and reactivate on a paid plan can charge or end what they pay for. Say what will happen and when, and wait for yes. A refund sends money back: show the amount and wait for yes.

Hands on: the subscription's new status. Check: call_appengine GET storefront/subscriptions/<id>. The status, renewalDate, pendingPlan, cancelAtPeriodEnd and the last history entry match what you said.

How renewals and failed payments run

The daily Subscription billing job (subscription-sweep, 06:00, for every business with a live subscription) runs each subscription whose next date has come:

  1. Paused with a resume date: resumed.
  2. A plan change scheduled for period end: switched.
  3. Set to end at period end: cancelled. Not set to renew: expired.
  4. Free: the next period starts. Paid: the saved card is charged the plan's price.

When a charge fails, the subscription goes past_due and the customer is emailed. It is tried again 1, 3 and 5 days after each failure. The fourth failure ends it: expired, "payment failed 4 times". Updating the card retries at once. A charge the bank wants the customer to confirm (3-D Secure) waits as pendingCharge until Stripe's webhook reports the result.

Every charge carries its own key for that subscription and that charge, so a run that repeats never takes the money twice.

Staff can run it now: POST storefront/subscriptions/check with { subscriptionIds?: [...] } answers how many ran, were charged, failed and ended. GET storefront/subscriptions/sweep gives the last full run.

Every paid period and every refund is also booked in Books as membership income.

What the customer sees

My Account › Subscriptions (/account/subscriptions) lists each subscription: the plan, price per period and number, a status badge (Waiting for your card, On trial, Active, Payment failed, Paused, Ending, Cancelled, Ended), the next payment, the card (Kept by Stripe or None yet), Member since, and a table of payments. Its buttons follow allowedActions: Change plan (now, or from the next payment), Add card / Update card, Keep my subscription, Cancel subscription.

The customer is emailed at each step, with the templates subscription-confirmation, -renewal, -paused, -resumed, -plan-changed, -cancelled, -reactivated, -expired and -payment-failed. A message template of the same name replaces the built-in one; see Email and SMS templates with merge fields.

Try it

Stop: with live Stripe keys, a test subscription takes real money. Try paid plans only with test keys.

  1. Sign in on the site as a test customer and open the plan's product. Press SUBSCRIBE.
  2. For a paid plan, pay on Stripe's page with 4242 4242 4242 4242.
  3. On /account/subscriptions, the plan shows Active (or On trial) with its next payment.

When it's done

Tell the person:

  • each plan: its price, period, trial and id;
  • which product sells it and its store page;
  • whether paid plans can be sold online (Stripe connected) or only free and counter plans;
  • that renewals, retries and endings run on their own every day, and that customers are emailed at each step.

Open Storefront › Subscriptions › Plans & Pricing.

What it can't do yet

  • Only Stripe charges renewals. PayPal and Helcim can't keep a card for later. A paid plan, a trial on a paid plan, Add card, renewal charges, a plan change that costs more, and reactivating a paid plan all need Stripe connected. Without it they are refused, and a paid plan's renewal goes past_due and then expired.
  • No reminder before renewal. A subscription-expiring template exists, but nothing sends it.
  • A free plan scheduled to move to a paid plan at period end is accepted without a card. At renewal it then fails and goes past_due.
  • The customer's account page has no Pause, Resume or Reactivate button (the calls work), and a free plan shows Next payment —.
  • Changing a plan's price doesn't change running subscriptions until their next renewal, when the plan's new price is charged.
  • A test engine on a private network can't receive Stripe's webhook, so a 3-D Secure charge there stays pending.

Don'ts

  • Don't sell a plan through the cart, cart.add or checkout.
  • Don't pass transaction for a shopper, or on behalf of a payment you haven't seen.
  • Don't rename a plan's name. Subscriptions and products find it by that name.
  • Don't make a paid plan on a business without Stripe and tell them it can be bought online.
  • Don't cancel, refund or change a real customer's plan without the person's yes.
  • Don't delete a subscription to "cancel" it. Cancel keeps its history and its money in Books.