Subscriptions and paywalls
Plans, free trials, offer codes, Stripe and Apple in-app purchases, and entitlements that gate features by what a user has paid for.
Last updated
A paywall comes down to one question your app asks before showing a paid feature: does this user have access right now? The hard part is everything behind it, which is plans, two payment providers, trials, promo codes and the rules for what a failed renewal or a cancellation means.
Anythink's payments system, AnythinkPay, is built into every project and answers that question for you. Stripe and Apple in-app purchases both end up as the same subscription record, so your app makes one call whichever provider a customer paid through. This page walks through setting it up end to end. Each step shows the Anythink dashboard, the CLI and an AI assistant (MCP), keeps to a few lines and links to the guide that covers it in full.
If you haven't used Anythink before, read Core concepts first.
Who it's for and what you'll build#
This is for anyone selling access to something: a web or mobile app, a members' area, a premium tier of a content product. You define the plans and the rules. Anythink holds the subscriptions, talks to Stripe and Apple, and gives your app a single access check.
You'll build a two-tier product, Pro monthly and Pro annual, sold on the web through Stripe and on iOS through Apple, with a 7-day free trial for new users and a promo code that adds 14 more days.
| Part | What it is | Guide |
|---|---|---|
| Plans | The products you sell: name, price, interval, optional Apple product id and tier rank. | Plans and subscriptions |
| Stripe | Your own Stripe account, connected to your project. Web checkout and subscriptions. | Take payments with Stripe |
| Apple in-app purchases | StoreKit 2 purchases verified on the server, for iOS digital goods. | Apple in-app purchases |
| Free trial and offer codes | One free window for users who have never subscribed, and codes that extend it. | Offer codes, free trials and entitlements |
| Entitlement | One call that says whether the signed-in user can use the feature. | How payments work |
| Workflows | Run steps when Stripe reports a subscription activating or ending. | Payments recipes |
The flow looks like this:
- A customer buys, either through Stripe checkout or an in-app purchase on iOS.
- Anythink records a subscription linked to their user, with a status and a current period.
- The app calls
me/entitlementas that user and readshas_access. If it'sfalse, the app shows the paywall. - Stripe changes raise events in your project, and a workflow can react.
To see the model in more depth first, read How payments and subscriptions work. If you're building the iOS side of an app, Mobile app backend covers sign-in, push and files around it.
Step 1: Create plans#
A plan is a product you define once. Each subscription copies its price, interval and trial length from the plan it was created from. Only project administrators can create and edit plans.
In the Anythink dashboard
- Open AnythinkPay, choose the Plans tab and select New plan.
- Enter a Plan name (your own label, such as
Pro monthly) and a Subscription name (what the customer sees at checkout). Leave Type as Web for Stripe checkout, or choose Mobile for a plan you sell through Apple. - Set Amount (GBP £), Billing period and Bill every. For £9.99 each month, enter
9.99, Monthly and1. - Optionally set Tier rank, a whole number from
1to100where higher is a better plan, so a promo code can be limited to premium plans. - Leave Active ticked and select Create plan. Repeat for
Pro annual.
With the CLI
anythink fetch /integrations/anythinkpay/subscription-plans --method POST --body \
'{"plan_name":"Pro monthly","name":"Pro monthly","description":"Full access","type":"web","amount":9.99,"currency":"gbp","billing_interval":"month","interval_count":1,"is_active":true}'
anythink fetch /integrations/anythinkpay/subscription-plans
There's no anythink pay subcommand for plans, so they go through fetch. PUT replaces every field, so send the whole plan when you edit one.
With an AI assistant (MCP)
List the active AnythinkPay subscription plans on this project.
There's no dedicated plans tool, so the assistant runs a fetch request against the plans endpoint through the cli tool. Connect an AI assistant sets up the MCP server.
The dashboard form prices new plans in pounds sterling. Create plans and manage subscriptions lists every plan field, the subscription statuses and the recovery tools.
Step 2: Connect Stripe#
Stripe is the rail for the web and for anything that isn't an iOS digital purchase. You connect your own Stripe account through Stripe Connect. Card numbers stay with Stripe and never reach Anythink.
In the Anythink dashboard
- Open Settings › Payments and stay on the Stripe tab.
- On Create New Account, leave Account Type as Express (Recommended), enter your Business Type, Email Address and Business Name, and select Create Stripe Connect Account. To use an account you already have, choose Connect Existing Account.
- Select Complete Account Setup and finish Stripe's onboarding form.
- Back in the dashboard the tab shows Fully Onboarded, with Charges and Payouts Enabled. Select Refresh Status if it still shows Pending Verification.
Once onboarding is complete, a plan syncs to Stripe when you save it. A plan's page shows Synced to Stripe, or Not yet synced to Stripe, in which case save it again.
With the CLI
anythink pay connect
anythink pay status
pay connect asks for the business type, the country code and an email address, creates the account and opens Stripe's onboarding link.
With an AI assistant (MCP)
The AI chat in the Anythink dashboard can't run pay commands. A locally running anythink-mcp server can, so ask your assistant to run pay connect and pay status.
Payments for what you sell are paid out to your connected Stripe account, minus Stripe's processing fee and Anythink's platform fee. Open Settings › Payments and the Fees & Calculator tab to see the fees that apply to your project and try an amount. Use Stripe's test cards, such as 4242 4242 4242 4242, while you try this. Take payments with Stripe covers one-off payments, saved cards and the Stripe events Anythink handles.
To start a Stripe subscriber, choose Add Subscriber on the Subscriptions tab, pick the user and the plan, and select Send checkout link. Send them the link, or open it yourself. Sell a subscription in 15 minutes walks through it.
Step 3: Add Apple in-app purchases#
Apple requires in-app purchases for digital goods sold inside an iOS app. Create your auto-renewing subscription products in App Store Connect, then give Anythink the settings to verify them. Your app doesn't need receipt-verification code of its own.
In the Anythink dashboard
- Open Settings › Payments and choose the Apple tab.
- Enter the App bundle ID, Default environment, App Store Connect Issuer ID and App Store Connect Key ID, paste the
.p8file into Private key (.p8), and select Save Apple settings. - Copy the URL under App Store Server Notifications (required) and set it for both Production and Sandbox in App Store Connect. Under Test ASSN delivery, select Test sandbox and Test production.
- Open AnythinkPay › Plans, edit each plan you sell on iOS and paste the product's identifier into Apple product ID.
With the CLI
anythink fetch /integrations/anythinkpay/apple-iap/credentials --method PUT \
--body "$(jq -n --rawfile key AuthKey_ABC123DEF4.p8 '{
bundle_id: "com.example.app",
environment: "sandbox",
asc_issuer_id: "<issuer id>",
asc_key_id: "ABC123DEF4",
asc_private_key_pem: $key
}')"
anythink fetch /integrations/anythinkpay/apple-iap/test-notification --method POST
GET on the same credentials path returns whether it's configured, never the key.
With an AI assistant (MCP)
The Apple settings include a private key, which shouldn't be typed into a chat prompt. Set them in the dashboard or the CLI rather than asking an assistant to enter them.
After a purchase or a restore, the app sends the signed transaction as the signed-in user:
POST /org/{project_id}/integrations/anythinkpay/subscriptions/apple/verify
Authorization: Bearer <user's access token>
Content-Type: application/json
{ "signed_transaction": "<JWS from a StoreKit 2 purchase>" }
Verify links the purchase to that user and returns the subscription with has_access. Repeating it is safe, so you can call it on every launch. If matched_known_plan is false, the product id isn't on any plan's Apple product ID, so don't unlock a feature on has_access alone. The first user to verify a transaction keeps it. Verify Apple in-app purchases covers restores, ownership and App Store Server Notifications.
Step 4: Run a free trial with an offer code#
There are two kinds of trial, and they're separate. A plan's own trial delays a subscriber's first charge. The app free trial gives every user who has never subscribed one window of access, with no payment provider involved. A promo code with a trial reward adds days to that second kind. This step builds the second: 7 days free, and a WELCOME14 code that adds 14.
In the Anythink dashboard
- Open Growth and choose the Engagement tab.
- Tick Offer a free trial to users who haven't subscribed, set Trial length (days) to
7and select Save. - Choose the Promo codes tab and select New promo code.
- Enter the Code
WELCOME14, set Reward to Extend free trial with14days, optionally set Valid until and a Total cap, and select Create promo code.
With the CLI
The trial switch is a project setting in the dashboard. The offer and its code work from the CLI:
anythink fetch /integrations/anythinkpay/offers --method POST --body \
'{"kind":"trial_extension","name":"Welcome trial","status":"active","per_user_redemption_cap":1,"redeemer_reward_json":"{\"type\":\"trial_extension\",\"days\":14}"}'
anythink fetch /integrations/anythinkpay/offers/{offer_id}/codes --method POST --body '{"slug":"WELCOME14"}'
anythink fetch /integrations/anythinkpay/offers/redemptions
Use the id the first call returns as {offer_id}.
With an AI assistant (MCP)
Create a promo offer called Welcome trial that adds 14 days to the free trial, once per user, then add the code WELCOME14 to it.
The assistant runs the same two fetch calls through the cli tool.
The trial starts the first time a user who has never subscribed calls the entitlement endpoint, and it runs once per user. When someone types a code, the app redeems it as them:
POST /org/{project_id}/integrations/anythinkpay/offers/me/redeem/WELCOME14
Authorization: Bearer <user's access token>
A rejected code still returns 200, so check success and show rejection_reason. Codes can also grant account credit or points, and can be limited to users on certain plans or tiers. See Offer codes, free trials and entitlements.
Step 5: Check entitlement in the app#
This is the paywall. The app calls one endpoint as the signed-in user and reads has_access. It already applies the access rule for Stripe, Apple, grace periods and cancelled-but-not-yet-expired subscriptions, and it falls back to the app free trial for users who have never subscribed.
The call needs the anythink_subscription_plans:read permission on the user's role. It isn't on the default roles, so grant it to the role your subscribers use. The same permission lets them list plans, redeem a code and read their balance.
In the Anythink dashboard
- Open Settings › Roles and permissions and select the role your subscribers use.
- Make sure API Access is ticked.
- Under Anythink Permissions, find
anythink_subscription_plans, tick Read and save the role. - To see what a subscriber sees, open AnythinkPay › Subscriptions and check the subscription's Status.
trialingoractivemeans access.
With the CLI
anythink fetch /integrations/anythinkpay/subscriptions/me/entitlement
anythink fetch /integrations/anythinkpay/subscriptions/by-user/61
Give your app users' role read access to anythink_subscription_plans in the dashboard, under Roles. me/entitlement reads as the user the CLI is signed in as. by-user/{id} lets an administrator read any user's subscriptions.
With an AI assistant (MCP)
Check whether I currently have access to the paid plan.
The assistant runs the entitlement fetch through the cli tool.
In your app, use the signed-in user's token. An API key has no subscriber behind it, so it can't ask about "me".
const base = `https://api.my.anythink.cloud/org/${projectId}/integrations/anythinkpay`;
export async function canUsePaidFeature(accessToken: string) {
const res = await fetch(`${base}/subscriptions/me/entitlement`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`Entitlement check failed: ${res.status}`);
const entitlement = await res.json();
if (entitlement.has_access) return { allowed: true, onTrial: entitlement.is_trial };
return { allowed: false, reason: entitlement.status ?? "no_subscription" };
}
When has_access is false, status says why: expired, revoked, unpaid or trial_expired, which is your cue to show the paywall with the plans from GET .../subscription-plans. To gate one plan rather than any subscription, compare product_id with the plan's name. Ask GET .../payment-options?platform=ios&storefront=GBR which provider to offer on a platform. The call isn't pushed to your app, so make it on launch and when the app returns to the foreground, not on every screen. Payments recipes has this and the other builds in full.
Step 6: React to subscription events#
When Stripe reports a change, Anythink raises an event in your project, and a workflow with an Event trigger can act on it. This step records a win-back task when a Stripe subscriber's access ends, using Subscription Expired.
Create a winback_tasks entity with subscriber_id (integer) and plan, provider and ended_status (text) first. See Model your data.
In the Anythink dashboard
- Open Workflows and select New Workflow. Name it
subscription-ended. - Set Event Type to Subscription Expired and select Create Workflow.
- Select Add Step and choose Create Data. Give it the Key
create_task. - Set Entity to
winback_tasksand the Payload to the fields below, make it the start step and select Enable Workflow.
{
"subscriber_id": "{{ $anythink.trigger.data.subscriber_id }}",
"plan": "{{ $anythink.trigger.data.name }}",
"provider": "{{ $anythink.trigger.data.provider }}",
"ended_status": "{{ $anythink.trigger.data.status }}"
}
With the CLI
anythink workflows create subscription-ended \
--trigger Event --event SubscriptionExpired --enabled
anythink workflows step-add <workflow_id> create_task \
--name "Create win-back task" --action CreateData --start --enabled \
--params '{
"entity_name": "winback_tasks",
"payload": "{\"subscriber_id\":\"{{ $anythink.trigger.data.subscriber_id }}\",\"plan\":\"{{ $anythink.trigger.data.name }}\",\"provider\":\"{{ $anythink.trigger.data.provider }}\",\"ended_status\":\"{{ $anythink.trigger.data.status }}\"}"
}'
With an AI assistant (MCP)
Create an entity called winback_tasks with an integer subscriber_id and text fields plan, provider and ended_status. Then create a workflow called subscription-ended that runs on the SubscriptionExpired event, and enable it.
The assistant creates the entity and the workflow through the cli tool. Add the Create Data step in the dashboard or with the CLI command above, because its parameters contain {{ $anythink... }} expressions that the cli tool doesn't accept.
The trigger data is the subscription, with snake_case fields such as subscriber_id, name, provider and status. Subscription Expired fires when access ends, not when a subscriber turns off renewal, so the task appears when they actually lose access. Subscription Activated fires when Stripe reports a subscription active or trialing, and again on later updates, so make any step you attach to it safe to repeat. Swap Create Data for Send An Email or Send a Push Notification to message the subscriber. Payments recipes has the full recipe, and how to rehearse it without Stripe.
What you get, and the limits#
What you get
- Plans you define once, with price, interval, optional Apple product id and tier rank.
- Stripe checkout and subscriptions through your own connected Stripe account.
- Apple in-app purchases verified on the server and held in the same subscription shape.
- An app free trial, promo codes and referral programmes, with rewards in trial days, points or account credit.
- One entitlement call that gates a feature the same way for every provider.
- Workflow events for Stripe subscription and payment changes, and a timeline of events on each subscription.
Limits to plan around
- Apple is iOS only. Android and web go through Stripe checkout.
- Workflow events come from Stripe only. Apple purchases update subscriptions and their history but don't start workflows. A Stripe renewal's invoice is recorded as a payment but doesn't raise Payment Succeeded.
- Apple subscriptions change only when Apple notifies you. A missed notification isn't corrected until the next one for that subscription. Cancel, Resume and Re-sync work on Stripe subscriptions only, and Apple subscribers manage theirs in the App Store.
- One payer per subscription. There's no team or per-seat billing. You can share read access to a subscription with other users, but that doesn't change who is billed.
- Apple ownership is first-wins. The first user to verify a transaction keeps it, and an administrator reassigns it with Relink / manage users.
- Two kinds of trial. The app free trial runs once per user and never restarts. A plan's own trial is a separate setting. Promo-code trial days extend only the app free trial.
- Balances aren't spent for you. AnythinkPay stores credit and points and lets you adjust them, but it doesn't apply a balance to a charge. That logic lives in your own workflow or backend.
- Subscribers need a permission.
anythink_subscription_plans:readisn't on the default roles, so grant it to the role your subscribers use. - Plans are priced in the dashboard in pounds sterling.
- The payments service has to be reachable. Plan creation and listing work without it. The entitlement call for a user who has subscribed, and Apple verification, return
502when it's down. - Payouts and fees. Stripe payments are paid out to your connected account minus Stripe's processing fee and Anythink's platform fee, which Fees & Calculator shows. Apple's payouts and fees are handled between you and Apple.