powered by

Payments recipes

Updated Sep 30, 2026
Three full builds using AnythinkPay — a mobile paywall, a subscription-events workflow, and a revenue view joined across payments and users.

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.

Gate a feature behind a paywall

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:

http
GET /org/{orgId}/integrations/anythinkpay/subscriptions/me/entitlement

The client-side logic:

json
{
  "has_access": true,
  "provider": "stripe",
  "status": "trialing",
  "expires_at": "2026-10-06T17:49:06Z",
  "trial_length_days": 0,
  "pending_product_id": null
}
text
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.
  • Cache the response for the length of an app session rather than calling it on every screen — it's not push-updated, so a webhook-driven status change (a renewal, a refund) won't reach an already-open app until the next 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.

Email and push on subscription events

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.

Workflow 1 — internal notification on new subscription

Key Step Why
notify_team Send an email Tell the team a new subscriber signed up

notify_team:

json
{
  "to": "sales@example.com",
  "template": "internal_new_subscriber",
  "data": {
    "provider": "{{ $anythink.trigger.data.provider }}",
    "status": "{{ $anythink.trigger.data.status }}",
    "amount": "{{ $anythink.trigger.data.amount }}"
  }
}

Workflow 2 — remind the subscriber on payment failure

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:

json
{
  "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.
  • These events arrive from AnythinkPay's Stripe and Apple webhook handlers, not from your own code creating the subscription or payment — so this workflow reacts the same way whether the subscription came from your dashboard, your app, or a provider-side change.

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.

Revenue view across users, payments and subscriptions

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:

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

http
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. Set payable_id when 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.

Next steps

How payments and subscriptions work

Payments recipes | Anythink Docs