Each recipe is a complete build you can reproduce. They show AnythinkPay working with the rest of Anythink: entitlements gating a mobile feature, a workflow reacting to billing events, and a materialised view turning payments and subscriptions into a reporting grid.
Goal: a mobile app that shows a paid feature only to subscribers with current access, and a paywall otherwise — checking one endpoint on launch, not re-implementing status logic client-side.
The call, made with the signed-in user's own Anythink token:
GET /org/{orgId}/integrations/anythinkpay/subscriptions/me/entitlement
The client-side logic:
{
"has_access": true,
"provider": "stripe",
"status": "trialing",
"expires_at": "2026-10-06T17:49:06Z",
"trial_length_days": 0,
"pending_product_id": null
}
if response.has_access:
show the feature
else if response.trial_length_days > 0 and response.status is null:
show "start your free trial" (app engagement trial, not yet started)
else:
show the paywall, with response.status (e.g. "expired", "revoked") to pick the right message
How it works:
has_access is the only field you need to branch on for the gate itself — it already accounts for Stripe grace periods, Apple's grace_period status, and cancelled-but-not-yet-expired subscriptions. See the access rule in Create plans and manage subscriptions.pending_product_id lets you show "switching to Basic on your next renewal" for an Apple subscriber who's scheduled a downgrade, without a separate call.Adapt it: for a feature gated to a specific plan tier rather than "any active subscription", compare product_id (the plan's name) or add a plan lookup keyed on it — has_access alone doesn't distinguish plans.
Goal: the moment a subscription is created, notify your team internally, and the moment a payment fails, push the subscriber a reminder to update their card.
Trigger: Event, SubscriptionCreated (first workflow) and PaymentFailed (second workflow) — see Triggers and scheduling for the full AnythinkPay event list.
| Key | Step | Why |
|---|---|---|
notify_team |
Send an email | Tell the team a new subscriber signed up |
notify_team:
{
"to": "sales@example.com",
"template": "internal_new_subscriber",
"data": {
"provider": "{{ $anythink.trigger.data.provider }}",
"status": "{{ $anythink.trigger.data.status }}",
"amount": "{{ $anythink.trigger.data.amount }}"
}
}
| Key | Step | Why |
|---|---|---|
find_subscriber |
Read data | Look up the subscriber's push token via your own subscriber_id link, or use the payment's payable_id if it's set |
remind |
Send a push notification | Ask them to update their card |
remind:
{
"title": "Payment failed",
"body": "We couldn't process your last payment. Update your card to keep your subscription.",
"click_action": "/account/billing",
"audience": { "kind": "user", "user_id": "{{ $anythink.trigger.data.payable_id }}" }
}
How it works:
SubscriptionCreated and PaymentFailed both fire with $anythink.trigger.id as 0 — the useful data is in $anythink.trigger.data, the subscription or payment record itself. See the AnythinkPay row in Triggers and scheduling.payable_id on a payment is the Anythink user it's attributed to — it's set when a payment came from a subscription with a known subscriber, but can be null for one-off payments created without a payable_id. Add a trigger filter or a Condition step to skip notifying when it's empty, rather than sending a push to no one.Adapt it: add a third trigger, PaymentSucceeded, to the reminder workflow with a Condition step checking whether the subscription had recently been in_billing_retry, and send a "you're all set" push to close the loop.
Goal: one grid, one row per user, showing whether they've ever paid, their lifetime payment total, and their current subscription status — without writing a cross-service SQL join by hand.
Build it as a materialised view:
{
"name": "Subscriber revenue",
"base_entity_name": "anythink_users",
"columns": [
{
"id": "has_paid",
"label": "Has paid",
"source": { "kind": "payments" },
"display": "exists"
},
{
"id": "lifetime_revenue",
"label": "Lifetime revenue",
"source": { "kind": "payments" },
"display": "value",
"value_field": "amount",
"value_order": "latest"
},
{
"id": "sub_status",
"label": "Current subscription status",
"source": { "kind": "subscriptions" },
"display": "value",
"value_field": "status",
"value_order": "latest"
}
],
"timeframe_preset": "90d",
"compare_enabled": true,
"row_limit": 500,
"sort_column": "lifetime_revenue",
"sort_direction": "desc"
}
POST /org/{orgId}/materialised-views
columns must be a JSON array (not a JSON-encoded string), and each column needs an id and a label.
then read it as a paged grid:
GET /org/{orgId}/materialised-views/{id}/data?page=1&page_size=50
How it works:
base_entity_name: anythink_users makes each row one user; every column attributes back to that user via Payment.PayableId or the local SubscriptionUsers link, not a raw foreign key on anythink_users itself.source.kind: payments / subscriptions reads from the same payment and subscription totals the dashboard's charts use, so the pinned TOTAL row is always the project-wide figure, independent of the current page or filters on the grid.display: "value" with value_field: "amount" sums each user's payments for the numeric column; the same shape with value_field: "status" instead reads the latest matching record's field, since status isn't summable.timeframe_preset: "90d" with compare_enabled: true shows a 90-day window with a trailing-90-day comparison delta in the column header — useful for "is revenue up or down this quarter", not just a lifetime snapshot.Note: A payment created without
payable_id(a one-off payment your own code created without attributing it to a user) still counts in the TOTAL but doesn't appear against any row. Setpayable_idwhen you create payments you want to see per user.
Adapt it: drop base_entity_name entirely for a totals-only card (lifetime revenue and active subscriber count with no per-user grid), or add a base_field column reading the user's own plan_tier field alongside the payments/subscriptions columns to segment revenue by your own data.