Payments recipes
Five builds with AnythinkPay: gate features, email on a payment, run a free trial with a promo code, react to an ended subscription, check access in an app.
Last updated
Each recipe is a complete build you can reproduce. They show AnythinkPay working with the rest of your project: roles that let subscribers read their own access, workflows that react to payments, promo codes that extend a free trial, and an app that asks one question to decide what to show. Every recipe is shown in the Anythink dashboard, with the CLI and with an AI assistant over MCP.
New to AnythinkPay? Read How payments and subscriptions work first, and connect Stripe and create a plan with Sell a subscription in 15 minutes.
| Recipe | You build |
|---|---|
| Give paying users access | A role that lets subscribers read their own entitlement, and a check your app can call |
| Email your team when a payment succeeds | A workflow on Payment Succeeded |
| Run a free trial with a promo code | The app's free trial and a code that adds days to it |
| React when a subscription ends | A workflow on Subscription Expired that creates a follow-up record |
| Check access from a mobile app | A launch check that picks a provider and reads entitlement |
Note: Payments from Stripe and Apple need a connected Stripe account or an App Store Connect app. The recipes below were run against a local project without either, so the steps that depend on a real payment are marked, and you confirm them with Stripe's test mode. Names such as winback_tasks and payment_received are examples. Swap in your own.
Give paying users access
Goal: a subscriber's own sign-in can ask "do I have access?", and your app shows the paid feature only when the answer is yes.
Before you start: you need a plan. See Create plans and manage subscriptions. Decide which role your subscribers have. A user gets a role when they register, and Roles and permissions covers choosing it.
The entitlement call needs the anythink_subscription_plans:read permission, the same one a subscriber needs to list plans, redeem a code and read their balance. Without it the call returns 403.
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 the row
anythink_subscription_plansand tick Read. - Save the role.
- To see what a subscriber sees, open AnythinkPay › Subscriptions and check the Status of their subscription. A status of
trialingoractivemeans access.
With the CLI
anythink roles list
anythink roles permissions add 104 anythink_subscription_plans --actions read
anythink roles permissions list 104
│ Entity │ Actions │
├─────────────────────────────┼─────────┤
│ anythink_subscription_plans │ read │
Use the id of a role that has API access, such as Standard User. Then read the signed-in user's entitlement, and an administrator can read any user's subscriptions:
anythink fetch /integrations/anythinkpay/subscriptions/me/entitlement
anythink fetch /integrations/anythinkpay/subscriptions/by-user/61
For a user who has never subscribed, on a project with the app's free trial off:
{
"has_access": false,
"subscription_id": null,
"provider": null,
"product_id": null,
"status": null,
"expires_at": null,
"auto_cancel_at": null,
"cancelled_at": null,
"is_trial": false,
"trial_ends_at": null,
"trial_days_remaining": null,
"trial_length_days": 7,
"trial_base_days": 0,
"trial_bonus_days": 0,
"pending_product_id": null,
"pending_at": null
}
me/entitlement always reads as the user whose profile the CLI is signed in as. by-user/{id} returns that user's subscriptions as a list, and is empty for someone with none.
With an AI assistant (MCP)
Give the Standard User role read access to anythink_subscription_plans, then check whether I currently have access.
The assistant runs roles permissions add and then fetch /integrations/anythinkpay/subscriptions/me/entitlement through the cli tool. Both run on a local server. The dashboard chat can list roles but not change their permissions. See MCP tool reference.
The check in your app:
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" };
}
How it works:
has_accessis the only field you branch on. It already applies the access rule for Stripe, Apple, grace periods and cancelled-but-not-yet-expired subscriptions. See How payments and subscriptions work.- When access is
false,statussays why:expired,revoked,unpaidandtrial_expiredare the usual ones. It isnullfor a user who has never subscribed and has no free trial. - To gate one plan rather than "any subscription", compare
product_id, which is the subscription's name, with the plan's name. - The call isn't pushed to your app. Call it on launch and when the app returns to the foreground, not on every screen.
- Use the signed-in user's token. An API key has no subscriber behind it, so it can't ask about "me".
Email your team when a payment succeeds
Goal: the moment a one-off payment completes, send your team an email with the amount and reference.
Before you start: verify a sending domain and create an email template of type payment_received that uses {{ amount }}, {{ currency }}, {{ description }} and {{ reference }}. See Send email from a workflow. You need a connected Stripe account to receive real events.
| Key | Step | Why |
|---|---|---|
notify_team |
Send An Email | Sends the template to your team |
In the Anythink dashboard
- Open Workflows and select New Workflow. Name it
payment-succeeded. - Leave Trigger Type as Event and set Event Type to Payment Succeeded.
- Select Create Workflow, then Add Step and choose Send An Email. Give it the Key
notify_team. - Enter your team's address as the To Email Address and pick the
payment_receivedtemplate. - On the Mappings tab, set
amountto{{ $anythink.trigger.data.amount }},currencyto{{ $anythink.trigger.data.currency }},descriptionto{{ $anythink.trigger.data.description }}andreferenceto{{ $anythink.trigger.data.reference }}. - Save the step, make it the start step, save the workflow and select Enable Workflow.
With the CLI
anythink workflows create payment-succeeded \
--trigger Event --event PaymentSucceeded --enabled
anythink workflows step-add <workflow_id> notify_team \
--name "Email the team" --action SendAnEmail --start --enabled \
--params '{
"to": "team@example.com",
"template_type": "payment_received",
"payload": "{\"amount\":\"{{ $anythink.trigger.data.amount }}\",\"currency\":\"{{ $anythink.trigger.data.currency }}\",\"description\":\"{{ $anythink.trigger.data.description }}\",\"reference\":\"{{ $anythink.trigger.data.reference }}\"}"
}'
workflows create prints the workflow's id. payload is a string that holds JSON, so its inner quotes are escaped.
With an AI assistant (MCP)
Create a workflow called payment-succeeded that runs on the PaymentSucceeded event, and enable it.
The assistant runs workflows create through the cli tool. Add the Send An Email step in the dashboard or with the CLI command above, because step parameters that contain {{ $anythink... }} expressions aren't accepted by the cli tool. Then ask the assistant to run workflows jobs <workflow_id> to read the results.
Rehearse it without Stripe: create the same workflow with an API call trigger, then post a sample payment to it.
anythink workflows create payment-alert --trigger Api --api-route payment-alert --enabled
# add the notify_team step to it as above, then:
anythink fetch /workflows/api/payment-alert --method POST --body \
'{"type":"web","provider":"stripe","amount":25.0,"currency":"GBP","status":"completed","description":"Event ticket","reference":"ord-1001","payable_id":61}'
anythink workflows jobs <workflow_id>
The job finishes as Success with the email queued, and its step output shows the amount, currency, description and reference your template received. Delete the rehearsal workflow when you are done.
How it works:
- The trigger data is the payment. Its fields are snake_case, and
$anythink.trigger.idis0, so always read from$anythink.trigger.data. payable_idis the Anythink user the payment is attributed to. It is set when your code attributes the payment to a user, and can be empty for a payment created without one.- The step queues one email. It refuses to send if any value still contains
{{, so a typo fails the job rather than emailing a placeholder. See Step types. - To start a workflow from a failed payment, use Payment Failed. It carries the same fields.
- Payment Succeeded fires for one-off payments, from a web checkout or a mobile payment. A subscription's renewal is recorded under AnythinkPay › Payments but doesn't start this workflow. Use Subscription Activated for renewals.
Adapt it: to email the customer rather than your team, add a Read Data step on anythink_users that filters id to {{ $anythink.trigger.data.payable_id }}, and send to {{ $anythink.steps.<key>.data[0].email }}. Build that step in the dashboard's workflow editor.
Check it worked: with Stripe in test mode, create a payment under AnythinkPay › Payments and pay with a Stripe test card. See Take payments with Stripe. The workflow's View Job History shows one job.
Run a free trial with a promo code
Goal: every new user gets 7 days free with no subscription, and a promo code you hand out adds 14 more.
Before you start: your app needs the entitlement call from Give paying users access, because that call is what starts a user's trial. Redeeming a code needs the same anythink_subscription_plans:read permission.
There are two kinds of trial, and they're separate. A plan's own Trial period (days) delays a subscriber's first charge. The app's free trial is for users who have never subscribed. A promo code with a trial reward adds days to the second kind only.
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, and optionally an Internal name. - Set Reward to Extend free trial and the days to
14. - Optionally set Valid from, Valid until and a Total cap, then select Create promo code.
With the CLI
The trial switch is a project setting you change under Growth › Engagement in the dashboard. Everything after that works 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}"}'
{
"id": "fb1e930c-41c2-47a6-a715-777ccc2a5063",
"name": "Welcome trial",
"kind": "trial_extension",
"redeemer_reward_json": "{\"type\":\"trial_extension\",\"days\":14}",
"per_user_redemption_cap": 1,
"status": "active"
}
Add the code the user will type, using the offer's id:
anythink fetch /integrations/anythinkpay/offers/fb1e930c-41c2-47a6-a715-777ccc2a5063/codes --method POST --body '{"slug":"WELCOME14"}'
anythink fetch /integrations/anythinkpay/offers/fb1e930c-41c2-47a6-a715-777ccc2a5063/codes
anythink fetch /integrations/anythinkpay/offers/redemptions
The last command lists redemptions across all offers.
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.
What your app does: when the user enters the code, call the redeem endpoint as them.
POST /org/{project_id}/integrations/anythinkpay/offers/me/redeem/WELCOME14
Authorization: Bearer <user's token>
A rejected code is still a 200. Check success:
{
"success": true,
"rejection_reason": null,
"redemption_id": "9d0c...",
"redeemer_reward_summary": "+14 day trial bonus",
"referrer_reward_queued": false
}
Then call me/entitlement. The bonus shows in trial_bonus_days, and trial_length_days becomes the 7 base days plus the bonus.
How it works:
- The trial starts the first time a user who has never subscribed calls
me/entitlement. It runs once per user and never restarts. The project's length is the base, and bonus days from redemptions are added when the entitlement is read. - While the trial runs,
has_accessistrue,is_trialistrueandstatusistrialing.trial_days_remainingcounts down. After it ends,has_accessisfalseandstatusistrial_expired, which is your cue to show the paywall. - A user who subscribes stops using the trial and is read from their subscription instead.
rejection_reasonsays why a code failed, for example the code doesn't exist, it has expired or reached its cap, or the user has already redeemed it. A user can't redeem their own personal code.per_user_redemption_capis how many times one user can redeem the offer. The dashboard form's Total cap limits redemptions across all users.- A code's reward can instead extend a subscription or add credit. See Offer codes, free trials and entitlements.
Note: The free trial is off until you tick the switch, and trial_length_days shows the configured length even while it's off. Use has_access, not trial_length_days, to decide what to show.
React when a subscription ends
Goal: when a subscriber's access ends, record a win-back task for your team with the plan and provider.
Before you start: create an entity winback_tasks with the fields below. See Data modelling.
| Field | Type |
|---|---|
subscriber_id |
integer |
plan |
text |
provider |
text |
ended_status |
text |
| Key | Step | Why |
|---|---|---|
create_task |
Create Data | Adds one winback_tasks record |
In the Anythink dashboard
- Open Workflows and select New Workflow. Name it
subscription-ended. - Set Event Type to Subscription Expired.
- Select Create Workflow, then Add Step and choose Create Data. Give it the Key
create_task. - Set Entity to
winback_tasksand the Payload:
{
"subscriber_id": "{{ $anythink.trigger.data.subscriber_id }}",
"plan": "{{ $anythink.trigger.data.name }}",
"provider": "{{ $anythink.trigger.data.provider }}",
"ended_status": "{{ $anythink.trigger.data.status }}"
}
- Make it the start step, save the workflow and select Enable Workflow.
With the CLI
anythink entities create winback_tasks
anythink fields add winback_tasks subscriber_id --type integer
anythink fields add winback_tasks plan --type varchar
anythink fields add winback_tasks provider --type varchar
anythink fields add winback_tasks ended_status --type varchar
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 runs entities create, fields add and workflows create 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.
Rehearse it without Stripe: create the workflow with --trigger Api --api-route subscription-ended instead, add the same step, and post a sample subscription:
anythink fetch /workflows/api/subscription-ended --method POST --body \
'{"type":"web","name":"Pro monthly","currency":"GBP","billing_interval":"month","status":"canceled","provider":"stripe","subscriber_id":61}'
anythink data list winback_tasks
Within a few seconds winback_tasks has one record with plan Pro monthly, provider stripe and ended_status canceled.
How it works:
- Subscription Expired carries the subscription as its trigger data.
subscriber_idis the Anythink user,nameis the plan's name andstatusis the Stripe status that ended it:canceled,unpaidorincomplete_expired. - It fires when access ends. A subscriber who turns off renewal keeps
activestatus until the period ends, and the event comes then, so the win-back task appears when they actually lose access. - Only Stripe subscriptions raise it. An Apple subscription that expires updates its status but doesn't start a workflow, so to catch those, run a Timed workflow that reads your own records.
- To record a failed renewal earlier, use Payment Failed, or watch for a subscription whose
statusispast_due.
Adapt it: replace Create Data with Send An Email or Send a Push Notification, using subscriber_id as the user. To look up the subscriber's email first, add a Read Data step on anythink_users in the dashboard's workflow editor.
Check access from a mobile app
Goal: on launch, your iOS or Android app decides how to sell, then shows the right screen from one entitlement answer.
Before you start: give your subscribers' role the permission from Give paying users access. For iOS, set up Apple in-app purchases and put each Apple product id on its plan. See Verify Apple in-app purchases.
In the Anythink dashboard
- Open Settings › Payments, choose the Apple tab and enter your bundle id and App Store Connect details. Select Save Apple settings.
- Open AnythinkPay › Plans, edit each plan and set its Apple product ID. Leave it empty for a plan you sell only through Stripe.
- Make sure your subscribers' role has the permission from the first recipe.
With the CLI
Check which provider your app should offer, and list the plans for your paywall:
anythink fetch "/integrations/anythinkpay/payment-options?platform=ios&storefront=GBR"
anythink fetch /integrations/anythinkpay/subscription-plans
{
"primary_provider": "stripe_web",
"providers": ["stripe_web"],
"web_checkout_allowed": true,
"storefront": "GBR"
}
This is the response on a project with no Apple settings saved. With Apple configured, an iOS storefront such as GBR returns apple first, and USA and most EU storefronts prefer stripe_web with apple second. Android, web and anything else that isn't ios always get stripe_web.
With an AI assistant (MCP)
List the active subscription plans on this project, then check whether I currently have access.
The assistant runs fetch /integrations/anythinkpay/subscription-plans and fetch /integrations/anythinkpay/subscriptions/me/entitlement through the cli tool.
The launch flow in your app:
- Sign the user in, then call
payment-optionswith the platform and, on iOS, the App Store storefront. Theprimary_providertells you which purchase flow to show. - Call
me/entitlement. Ifhas_accessistrue, show the feature and stop. - If it is
false, show the paywall with the plans fromsubscription-plans. - After an Apple purchase or a restore, send the transaction to
POST .../subscriptions/apple/verify. It returnshas_accessfor that purchase and links it to the signed-in user. Callme/entitlementagain to refresh the screen. - After a Stripe checkout, call
me/entitlementwhen the user returns to your app.
const base = `https://api.my.anythink.cloud/org/${projectId}/integrations/anythinkpay`;
export async function afterApplePurchase(accessToken: string, signedTransaction: string) {
const res = await fetch(`${base}/subscriptions/apple/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ signed_transaction: signedTransaction }),
});
if (res.status === 409) return { linked: false, reason: "bought_with_another_account" };
if (!res.ok) throw new Error(`Verify failed: ${res.status}`);
const result = await res.json();
return { linked: true, hasAccess: result.has_access, knownPlan: result.matched_known_plan };
}
How it works:
payment-optionsstops your app hard-coding the Stripe-or-Apple decision. It only reads your Apple settings and the storefront.- For a restore on a new device, send
original_transaction_idinstead ofsigned_transaction. That path also needs the App Store Connect issuer id, key id and private key saved in your project. - A
409means the purchase is already linked to a different account. The first account to verify keeps it. - When
matched_known_planisfalse, the product id isn't on any plan's Apple product ID. Treat it as an unrecognised product and don't unlock a feature onhas_accessalone. - Verify and entitlement are safe to repeat, so call them on every launch.