powered by

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

  1. Open Settings › Roles and permissions and select the role your subscribers use.
  2. Make sure API Access is ticked.
  3. Under Anythink Permissions, find the row anythink_subscription_plans and tick Read.
  4. Save the role.
  5. To see what a subscriber sees, open AnythinkPay › Subscriptions and check the Status of their subscription. A status of trialing or active means access.

With the CLI

bash
anythink roles list
anythink roles permissions add 104 anythink_subscription_plans --actions read
anythink roles permissions list 104
text
│ 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:

bash
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:

json
{
  "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:

ts
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_access is 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, status says why: expired, revoked, unpaid and trial_expired are the usual ones. It is null for 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

  1. Open Workflows and select New Workflow. Name it payment-succeeded.
  2. Leave Trigger Type as Event and set Event Type to Payment Succeeded.
  3. Select Create Workflow, then Add Step and choose Send An Email. Give it the Key notify_team.
  4. Enter your team's address as the To Email Address and pick the payment_received template.
  5. On the Mappings tab, set amount to {{ $anythink.trigger.data.amount }}, currency to {{ $anythink.trigger.data.currency }}, description to {{ $anythink.trigger.data.description }} and reference to {{ $anythink.trigger.data.reference }}.
  6. Save the step, make it the start step, save the workflow and select Enable Workflow.

With the CLI

bash
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.

bash
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.id is 0, so always read from $anythink.trigger.data.
  • payable_id is 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

  1. Open Growth and choose the Engagement tab.
  2. Tick Offer a free trial to users who haven't subscribed, set Trial length (days) to 7 and select Save.
  3. Choose the Promo codes tab and select New promo code.
  4. Enter the Code WELCOME14, and optionally an Internal name.
  5. Set Reward to Extend free trial and the days to 14.
  6. 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.

bash
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}"}'
json
{
  "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:

bash
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.

http
POST /org/{project_id}/integrations/anythinkpay/offers/me/redeem/WELCOME14
Authorization: Bearer <user's token>

A rejected code is still a 200. Check success:

json
{
  "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_access is true, is_trial is true and status is trialing. trial_days_remaining counts down. After it ends, has_access is false and status is trial_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_reason says 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_cap is 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

  1. Open Workflows and select New Workflow. Name it subscription-ended.
  2. Set Event Type to Subscription Expired.
  3. Select Create Workflow, then Add Step and choose Create Data. Give it the Key create_task.
  4. Set Entity to winback_tasks and the Payload:
json
{
  "subscriber_id": "{{ $anythink.trigger.data.subscriber_id }}",
  "plan": "{{ $anythink.trigger.data.name }}",
  "provider": "{{ $anythink.trigger.data.provider }}",
  "ended_status": "{{ $anythink.trigger.data.status }}"
}
  1. Make it the start step, save the workflow and select Enable Workflow.

With the CLI

bash
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:

bash
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_id is the Anythink user, name is the plan's name and status is the Stripe status that ended it: canceled, unpaid or incomplete_expired.
  • It fires when access ends. A subscriber who turns off renewal keeps active status 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 status is past_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

  1. Open Settings › Payments, choose the Apple tab and enter your bundle id and App Store Connect details. Select Save Apple settings.
  2. Open AnythinkPay › Plans, edit each plan and set its Apple product ID. Leave it empty for a plan you sell only through Stripe.
  3. 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:

bash
anythink fetch "/integrations/anythinkpay/payment-options?platform=ios&storefront=GBR"
anythink fetch /integrations/anythinkpay/subscription-plans
json
{
  "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:

  1. Sign the user in, then call payment-options with the platform and, on iOS, the App Store storefront. The primary_provider tells you which purchase flow to show.
  2. Call me/entitlement. If has_access is true, show the feature and stop.
  3. If it is false, show the paywall with the plans from subscription-plans.
  4. After an Apple purchase or a restore, send the transaction to POST .../subscriptions/apple/verify. It returns has_access for that purchase and links it to the signed-in user. Call me/entitlement again to refresh the screen.
  5. After a Stripe checkout, call me/entitlement when the user returns to your app.
ts
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-options stops 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_id instead of signed_transaction. That path also needs the App Store Connect issuer id, key id and private key saved in your project.
  • A 409 means the purchase is already linked to a different account. The first account to verify keeps it.
  • When matched_known_plan is false, the product id isn't on any plan's Apple product ID. Treat it as an unrecognised product and don't unlock a feature on has_access alone.
  • Verify and entitlement are safe to repeat, so call them on every launch.

Next steps