# Paywall changelog

Integration agents: read [the upgrade guide](https://paywallhq.com/upgrade.md) and [SDK manifest](https://paywallhq.com/vendor/manifest.json).

## 2026-10-14 — Price a plan for one exact Shopify plan

Release ID: 2026-10-14-exact-shopify-plan-prices

A Shopify plan price can now name one of Shopify's store plans exactly — Paused, Plus Trial, Starter and the rest — not only a tier.

- A plan's "Price per Shopify plan tier" list gains an "Exact Shopify plan" group with Shopify's own store plan names: Basic, Grow, Advanced, Plus, Plus Trial, Starter, Trial, Paused, Development, Staff Business, Retail, Lite, Agentic, Agentic Enterprise, Shop Component, Inactive and Other.
- A store gets the price for its exact plan first, then its tier, then "Every tier". A plan priced only for Paused stores is offered to paused stores alone — what Mantle's "Available by plan" did, and what the tiers could not say.
- Older names are read as Shopify's current ones (Pause and Build is Paused, Shopify Starter is Starter, Shopify Plus Trial is Plus Trial). Existing tier prices are unchanged.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app; it already sends the store's Shopify plan.

### Verify in your app

- A plan with an exact "Paused" price is offered to a paused store and not to a Basic store.

## 2026-10-13 — Stores on Shopify's Grow plan get their tier's price

Release ID: 2026-10-13-shopify-grow-tier

A store on Shopify's Grow plan now counts as the Shopify tier, so a plan priced per Shopify plan is offered to it.

- Shopify renamed its "Shopify" plan to "Grow". Paywall did not recognise the new name, so a Grow store matched no tier: a plan priced only for some tiers was not offered to it at all.
- A Grow store now counts as the Shopify tier. The tier is labelled "Shopify / Grow" on a plan's prices.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app.

### Verify in your app

- A store on the Grow plan is offered a plan that has a "Shopify / Grow" price, at that price.

## 2026-10-12 — Paying shops of a Billing API app land on their plan

Release ID: 2026-10-12-billing-api-paying-shops

On a Shopify app set to the Legacy Billing API, shops paying for a plan could stay on no plan; their named charge now grants it.

- A Shopify app linked first as App Pricing and then switched to the Legacy Billing API kept the App Pricing feed it had already read. That feed holds a nameless copy of every Billing API charge, and Paywall treated the copy as the real subscription and set the named charge aside — so a shop paying for "Basic Plan" held a subscription no plan mapping could match, and stayed on no plan.
- A Billing API link now reads only the Billing API feed, which is all it syncs: each charge keeps its name and its subscription id, and matches its plan by either.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app. On a Billing API app that showed paying shops on no plan, run Rebuild from raw once on its Shopify page.

### Verify in your app

- After Rebuild from raw, GET /v1/admin/apps/:app/shopify/unmatched lists no shop whose charge has a mapped name or a recorded subscription id.
- A shop paying for a mapped plan shows that plan on its customer page.

## 2026-10-12 — See why a paying Shopify shop is on no plan

Release ID: 2026-10-12-shopify-unmatched-shops

Two read-only admin API endpoints show which shops hold an active Shopify charge but no Paywall plan, and why each charge did not match.

- GET /v1/admin/apps/:app/shopify/unmatched lists every shop with an active Shopify subscription that Paywall has on no plan, with each charge's name, amount, subscription id and the reason it did not match: no charge name, a name missing from the plan mapping, or a test charge.
- GET /v1/admin/apps/:app/shopify/shops/:shop/feed shows one shop end to end: its Partner feed events in order, the subscriptions they became, the payouts by charge and its current plan.
- Both only read. They accept the master admin key or the app's own agent token, like every other admin endpoint.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app.

### Verify in your app

- GET …/shopify/unmatched with the app's agent token returns a count per reason and the affected shops.
- GET …/shopify/shops/<shop>.myshopify.com/feed returns that shop's events and why its charge did or did not match.

## 2026-10-10 — Re-checking a large Shopify app's shops no longer skips some of them

Release ID: 2026-10-10-chunked-shop-rebuild

Rebuild from raw, Copy plan values, saving the plan mapping and turning test charges on or off now re-check shops in batches, so every shop of a large app lands on its plan.

- Re-checking an app's shops ran in one go. On an app with well over a thousand shops it ran past a limit partway through, and the shops after that point were left on whatever plan they had — some paying shops on no plan — with only a vague error shown.
- It now runs in batches of 25 shops, the page carrying on by itself and showing progress ("Re-checking shops: 450 of 1,763…"). Copy plan values from Mantle, saving the plan mapping and changing "Honour test charges" start the same batched re-check.
- Shops that fail to re-check are counted and named in the result instead of being skipped silently.
- Admin API: POST /v1/admin/apps/:app/shopify/rebuild re-checks one batch; call it again with ?offset=<nextOffset> until nextOffset is null.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app. If you imported a large Shopify app from Mantle before this fix, run Rebuild from raw once on its Shopify page.

### Verify in your app

- On a large app, Rebuild from raw reports progress batch by batch and ends with "Re-derived all N shops".
- A shop with an active Shopify charge holds its mapped plan after the rebuild.

## 2026-10-10 — Development stores can try a live Shopify app's plans

Release ID: 2026-10-10-test-charges-in-live-mode

"Honour test charges" now works whether the Paywall app is in test or live mode, so a development store that buys a plan gets it — and real merchants still always pay.

- A Shopify app's link setting "Honour test charges" used to work only while the Paywall app was in test mode. Switching a production app to live meant a development store could subscribe in Shopify yet stay blocked in the app, so developers and testers could not try the app's plans.
- It now works in test and live mode alike. Shopify itself decides what is a test charge: a development store can only be charged in test, a real store is always charged for real, so the setting unlocks plans on development stores only — as Mantle did.
- Test charges stay out of MRR, revenue and every report, whichever mode the app is in. Switching an app between test and live no longer changes which shops hold a plan.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app. To let development stores buy plans on a live app, tick "Honour test charges" on the app's Shopify page and save the link.

### Verify in your app

- With the app live and the box ticked, a development store that subscribes to a plan gets its features at once.
- With the box unticked, a development store's test charge grants nothing; real stores are unaffected either way.

## 2026-10-09 — Grandfathering keeps the shops that have your app, not every shop that ever had it

Release ID: 2026-10-09-grandfather-installed-shops-only

Import from Mantle's Grandfather installs now leaves out shops that have uninstalled, and a grandfathered plan ends when its shop uninstalls — so a returning shop starts like any new install.

- A full Shopify history sync knows every shop that ever installed your app, most of them long gone. Grandfather installs used to put all of them on Mantle's free plan or the legacy plan, so any of them coming back got the paid features for free. It now skips a shop that has uninstalled — by Mantle's own record or by Shopify's feed — and the preview says how many it left out.
- A grandfathered plan now ends when its shop uninstalls, on the next sync. If the shop comes back, it starts on your default plan like any new install. A store Shopify pauses or freezes still has your app and keeps its plan.
- Plans paid through Shopify, plans granted by hand and per-shop extras are not affected.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app. Shops already grandfathered that have since uninstalled lose that plan on the next sync (or Rebuild from raw).

### Verify in your app

- Preview shops in Import from Mantle reports uninstalled shops as left on the default.
- A grandfathered shop that uninstalls holds the default plan after the next sync, and keeps it when it reinstalls.

## 2026-10-09 — Shopify charges are matched to plans by their subscription, not only their name

Release ID: 2026-10-09-match-charges-by-subscription-id

Import from Mantle now records each shop's Shopify charge against the plan it belongs to, so two Mantle plans that share a name — or a hidden twin of a public plan — each keep their own shops.

- Paywall used to know which plan a Billing API charge was for by the charge's name alone, and one name can point at one plan only. Two Mantle plans called "Basic" with different features could not both be imported, and one of them had to be skipped.
- Import from Mantle now reads, for every subscribed shop, the Shopify subscription its Mantle plan is billed through, and records that subscription against the Paywall plan the Mantle plan is copied into. From then on the shop is matched by its own subscription, whatever the charge is called. Charges nobody recorded are matched by name exactly as before.
- When several copied Mantle plans share a charge name, the name keeps pointing at the public one, and the copy summary says which other plans' shops are matched by subscription instead.
- The plan picker in Import from Mantle can create a plan under a key you type, so a hidden plan can keep a plan of its own (say plus_legacy) instead of being merged into the public one or skipped.
- When your app reports its live Billing API subscription to Paywall (refreshShopifyShop), the subscription's id is now used the same way — first the id, then the name.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Nothing to change in your app. To record subscriptions for an app already imported, run Import from Mantle → Connect and preview again while Mantle still answers, then Copy plan values with the same choices.

### Verify in your app

- After the copy, the summary reports how many shops' charges were recorded by subscription.
- A shop on a hidden Mantle plan that shares its name or price with a public one holds the plan its hidden plan was copied into.
- A shop whose charge was not recorded keeps the plan its charge name maps to.

## 2026-10-01 — Move a Shopify app off Mantle without touching its components

Release ID: 2026-10-01-use-mantle-drop-in

A new SDK file, hub-mantle.js, is a drop-in for Mantle's React provider and useMantle() hook, fed by Paywall. Components keep reading the same fields; only the import changes.

- hub-mantle.js gives Shopify apps a MantleProvider and useMantle() with Mantle's shape — customer, customer.features, subscription, plans, loading, isFeatureEnabled, limitForFeature, subscribe, cancelSubscription and refetch — answered from Paywall. Feature checks follow Mantle's rules exactly: a limit allows a count below it, and -1 means unlimited.
- One server call, loadPaywallMantle(), gathers what the provider needs: the shop's plan and features, and the plans it is offered at its own prices. The secret key stays on your server.
- Monthly and annual prices are listed as separate plans, the way Mantle listed them, with ids "<planKey>" and "<planKey>:annual". The shop's current subscription uses the same ids, so finding the current plan keeps working.
- A shop's subscription shows what Shopify actually charges when Paywall knows it, in the currency Shopify charges. Features a plan does not grant are filled in as off, so code reading a feature's value never meets a missing key.
- Subscribing still needs one route in your app: Mantle created the Shopify charge itself, and creating one needs the shop's Admin token, which Paywall never holds. The provider calls your route and hands back its confirmation URL.
- Every field of Mantle's customer, subscription, plan and feature objects is present, so code written for Mantle never meets a missing one. Paywall fills in what it knows — the shop's name, when it installed, and its Shopify subscription's activation, renewal, trial and cancellation dates — and returns Mantle's empty shape for what has no counterpart: usage, reviews, payment methods and custom fields. Custom field values set by hand in Mantle do not carry over.
- Import from Mantle: when several Mantle plans go into one Paywall plan — three called "Basic" at different prices, say — the public one now sets the price new merchants are offered, and each hidden plan's price stays with the shops on it. The cheapest one used to win, so a hidden discount could become the public price. The plan's trial now comes from that same public plan too.
- Import from Mantle records which Mantle plans went into each Paywall plan — name, price, public, hidden or custom, how many shops, and what became of its price — and the summary after copying says so, e.g. "basic ← Basic $4.99/mo, Basic Annual $49/yr, Basic $3/mo (hidden; 24 shops keep this price)". The record outlives the preview and is readable at GET /v1/admin/apps/:app/shopify/mantle/sources.
- Grandfather installs now asks before grandfathering installs that never subscribed — a checkbox, off by default. Shops on Mantle's free plan are always put back on it; installs that never subscribed go onto the legacy plan only if Mantle let them use the app, which differs by app. Leaving it off keeps them on "No plan", as they were.
- The legacy plan Grandfather creates now really has every feature. It only counted features already published, so when Grandfather ran straight after Copy plan values — the usual order — the plan was created empty and those shops got nothing.
- A shop that subscribed, cancelled and comes back is offered only what is left of its first trial — cancelled on day 2 of 7 and back on day 3, it gets 5 days; back after the date, none. It used to be offered a fresh trial if it returned within the hour Shopify's feed takes to report a Billing API charge, because the cancel wiped the only record that it had subscribed. That record is now kept for good.
- The Shopify plans answer (GET /v1/shopify/plans) now also returns the shop's stored details, which is where those values come from, and the app's feature flags with their types — so a number flag reads as a Mantle limit, with -1 for unlimited, without the app listing its limit keys.

### Integration changes

Breaking change: no. SDK revision: 2026-10-01.1.

1. Download hub-mantle.js next to hub-server.js. In a loader, call loadPaywallMantle(hub, { shop }) and return the result.
2. Replace Mantle's MantleProvider with the one from hub-mantle.js: pass the loader's result as paywall, and onSubscribe, onCancel and onRefetch. Limit features are recognised from their Paywall type; nothing to list.
3. Add a subscribe route that calls hub.shopifySubscribe() and, for the Billing API, appSubscriptionCreate, returning { confirmationUrl } — or { url } for App Pricing.
4. Change each `import { useMantle } from "@heymantle/react"` to import from hub-mantle.js. Replace any hard-coded Mantle plan ids with Paywall plan keys.
5. The other SDK files changed only their revision line; there is no need to re-download them.

### Verify in your app

- A shop on "No plan" has no subscription, and every gated feature reads as off.
- A shop on a paid plan sees its plan marked current, at the price Shopify charges, and isFeatureEnabled/limitForFeature give the same answers Mantle gave for the same plan.
- Choosing a plan sends the merchant to Shopify's confirmation page, and after approving, the new plan's features are on.

## 2026-09-30 — A retired Mantle plan is imported retired, not quietly put back on sale

Release ID: 2026-09-30-retired-plans-keep-nobody

"Unlisted" was answering two different questions the same way: a plan sold to a chosen few and a plan nobody may buy any more both kept their shops.

- Import from Mantle now tells a Custom plan from a Hidden one. A plan Mantle offers by customer, by tag, or by Shopify plan is still on sale to a chosen few, so the shops on it are assigned to it — hiding it from the merchants who bought it would be the bug. A plan Mantle has Hidden is retired: it arrives unlisted and assigned to nobody, so the shops on it keep everything they have and no one, including them, is offered it again.
- Nothing moves an entitlement either way. A shop on a retired plan keeps that plan and its features; the only difference is whether the plan appears on anyone's plan page as something to take up.
- Where Mantle gives no availability at all, the shops are still assigned and the summary says the assignment was a guess, so you can clear it on the plan page if the plan was meant to be retired. Assigning a shop later is one click; a merchant who cannot find the plan they bought is not.
- A Custom plan now arrives with "show this plan alone" turned on, so the shops assigned to it see it instead of the public plans — the way Mantle showed a custom-priced merchant their own plan. It is set once, on the run that makes the plan unlisted; after that the checkbox is yours and a later run will not turn it back on. It is never set on a plan whose availability Mantle did not state.
- The import never removes an assignment. A shop you assigned by hand was a decision, and re-running the import leaves it alone.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. If you imported before this, re-run "Copy plan values into Paywall": plans Mantle has Hidden keep the shops already assigned to them, so clear those on the plan page if you want the plan truly retired.

### Verify in your app

- The summary after the copy names each unlisted plan and says whether it kept its shops or was retired.
- A shop on a retired plan still holds it — check the customer page — but does not see it among the plans it can subscribe to.

## 2026-09-29 — Grandfathering now finds the free-plan shops it was built for

Release ID: 2026-09-29-grandfather-free-plan-shops

A shop on a free Mantle plan was being treated as a paying shop and left behind — the exact case the feature exists to rescue.

- Mantle's answer about a shop was copied into a smaller internal shape before the rules read it, and that copy dropped the subscription plan's price. A free plan therefore looked like a paid one, so grandfathering skipped those shops and left them on "No plan" with nothing — while reporting success. Shops that had never subscribed at all took a different branch and were handled correctly, which is what made it hard to spot.
- The plan created for shops that installed and never subscribed is now called "Free (legacy)" rather than an internal name. What those shops have is a free plan; that they arrived without ever subscribing is your business, not the merchant’s, and the plan key still carries the distinction in the dashboard. Plans already created keep their name — rename them on the plan page if you want.
- A grandfathered shop now reaches the app, not just the database. The grant was written without rewriting the shop’s cached entitlement, and identify — the endpoint your app actually calls — reads that cache, so a run could report every shop rescued while their apps went on seeing the plan they had before.
- Mantle’s price is read the same way everywhere now. It arrives as a plain amount, as a shop-currency presentment amount, or as a string, and only one of those was being read where it mattered — so a free plan priced "0.00", or priced only in the shop’s currency, was treated as paid and skipped.
- A run only reports the shops it actually put on a plan. Importing extras also worked out where each shop would go, and said so, which read as though it had moved them.
- The preview now also shows the shops it would put on the legacy plan. Before, it reported nothing at all, because the plan it names does not exist until the run creates it — so the button had to be pressed blind.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. If you already ran Grandfather installs, run it again: shops that were correctly granted are skipped, and the free-plan shops it missed are picked up.

### Verify in your app

- Preview shops names the free-plan shops as well as the never-subscribed ones, and the counts match what the run then reports.
- A shop on a free Mantle plan holds that plan afterwards, rather than the zero-feature default.

## 2026-09-25 — Keep every existing shop working when you leave Mantle

Release ID: 2026-09-25-grandfather-existing-installs

A shop on a free Mantle plan, or one that installed and never subscribed, has no Shopify charge behind it — so nothing could ever grant it a plan here. One button puts them all on one.

- Shopify tells Paywall which shops are paying. It cannot tell Paywall about a free plan or a bare install, because no charge exists — so those shops would land on "No plan" with every feature off the day you switch over, even though Mantle had them working. "Grandfather installs" on the Shopify page puts each of them on a plan instead.
- A shop on Mantle's free plan goes onto the Paywall plan that maps to it. A shop that never subscribed to anything goes onto a new "Legacy install" plan, created with every feature flag you have today, unlisted and unpriced so nobody can buy their way onto it. Shops already on a plan, and shops Shopify is paying for, are left alone.
- The grant yields: it survives a sync that finds no subscription, but steps aside the moment that shop actually buys a plan, so Shopify takes over cleanly. A manual grant would have blocked it — the merchant would pay and stay where they were.
- It is one plan, so it stays yours to change: edit what "Legacy install" grants and every shop on it follows at the next publish. A feature added later is off there until you tick it.
- It is deliberately its own button, not part of the import — running it months later would grandfather shops that churned in between. Preview shops first: it names every shop it would move and the plan it would move them to.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. Do this last, at the cutover: copy the plans, then Preview shops, then Grandfather installs, then publish.
2. Check what "Legacy install" grants before you publish — it starts with every feature flag, which is deliberately generous. Narrow it there, once, rather than per shop.

### Verify in your app

- A shop on Mantle's free plan and a shop that never subscribed both report features after the run; a shop that installs afterwards gets nothing.
- A grandfathered shop keeps its plan through a sync that finds no subscription, and moves onto a paid plan by itself once it subscribes.

## 2026-09-24 — "No plan" is always the default, and a hidden Mantle plan is found however it arrives

Release ID: 2026-09-24-no-plan-is-always-the-default

What a shop holds when it has bought nothing is one answer — nothing — and a free Mantle plan is now a plan like any other.

- Import from Mantle always creates "No plan" and always makes it the default, taking the default back from another plan if one holds it. A free Mantle plan never becomes the default, however Mantle lists it: it becomes a plan of its own, and the shops that should have its features are put on that plan. Briefly a public free plan took the default instead, which handed every unsubscribed shop that plan's features.
- A plan hidden in Mantle is now recognised as hidden. Mantle answers "is this plan hidden?" three ways and they disagree — a plan its own dashboard shows as Hidden comes back with availability "public" AND public true, and only visible false tells the truth. Import now reads all three: a plan is unlisted unless Mantle lists it to somebody, and one shop seeing it listed keeps it public. A Custom plan is still caught by the public flag as before.
- A plan's visibility now takes the strictest sighting. Mantle repeats a plan's details for every shop that sees it and the copies disagree — a hidden plan often arrives only as a shop's subscription, where the public flag is missing entirely, while the shop it is offered to carries it. Any shop saying "not public" settles it, so a hidden plan is no longer imported as public because of the order the shops were read in.
- Making a plan unlisted now applies to every plan the copy targets, not only the ones that run creates — so an app imported before this rule is put right by running the import again. It only ever tightens: a plan already unlisted in Paywall is never re-published from Mantle.
- If "No plan" is found granting features, the import says so in its summary and leaves it alone. A shop with no subscription holds the default plan, so anything granted there is granted to everyone who never paid — but a grant is your decision to remove, not the import's.
- A Mantle plan can no longer be copied into "No plan" at all, and the page never suggests it — not even when an earlier import left a plan mapping pointing there. That mapping is what kept offering to pour a free plan's features into the plan every unsubscribed shop holds.
- Choosing a different Paywall plan for a Mantle plan now repoints its mapping entry instead of leaving the old one in place, and the summary names what moved. A stored mapping still wins over a guess — but not over a choice you just made.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. On the app's Shopify page, run Import from Mantle again: read all shops, then copy the plans. The summary names the plans made unlisted, says if the default moved back to "No plan", and warns if "No plan" grants anything.

### Verify in your app

- After copying, "No plan" is the default and no other plan is, whether or not Mantle has a free plan.
- A plan hidden in Mantle is unlisted in Paywall with its Mantle shops assigned, even when the first shop read was the one subscribed to it.
- A shop with no subscription reports no features.

## 2026-09-24 — Hide a plan without hiding it from its own subscribers

Release ID: 2026-09-24-plan-visibility-and-counts

A shop always sees the plan it is on, whatever the plan's visibility, and every plan now shows how many customers are on it.

- Taking a public plan unlisted stops new shops buying it and changes nothing for the shops already on it: they keep the plan, and they still see it on their plan page with "Current plan" on it. Before, their own plan vanished from the list and the app looked as though they were on nothing.
- This needs no setup — it follows each shop's own subscription, so there is no list to fill in when you flip a plan and nothing to keep up to date. The shop list on the plan stays for deliberate additions: a new merchant you want on an old plan, or a shop returning after an uninstall.
- The Plans table shows how many customers are on each plan, and the plan page says how many shops hold it right now — so you can see who a plan is hiding from before you hide it. The default plan's count includes everyone who has bought nothing, which is where most shops of a Shopify app sit.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required, and nothing to configure.

### Verify in your app

- Take a plan that has subscribers from public to unlisted: those shops still see it with "Current plan", and no other shop sees it or can subscribe to it.
- The Plans table counts match: the paid plans add up to your subscribers, and the default plan holds the rest.

## 2026-09-24 — Import from Mantle keeps each plan's visibility, and its free plan

Release ID: 2026-09-24-mantle-import-visibility

A plan Mantle hides arrives unlisted with the shops that hold it, and a free plan becomes a plan of its own instead of being merged into "No plan".

- A Mantle plan marked not public — Custom or Hidden — is created unlisted, whatever it costs, and every shop Mantle shows on it is assigned it. Its own subscribers keep it; nobody else is offered it. Before, a hidden plan could arrive on sale to every shop.
- A free Mantle plan becomes a plan of its own instead of being merged into "No plan", and if Mantle does not list it publicly it is created unlisted and assigned to the shops that hold it.
- "No plan" is always there either way — it is how you say a shop has nothing, whether or not it is the plan a shop starts on.
- Either way a free plan is never merged into "No plan" — that put its features on the plan every shop holds before subscribing.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. On the app's Shopify page, run Import from Mantle again: read all shops, then copy the plans. The summary names the plans made unlisted and the shops on each.
2. Check which plan is the default afterwards, and publish. Plans created before this change keep the visibility they already have — set those by hand on the plan page.

### Verify in your app

- A plan hidden in Mantle is unlisted in Paywall after the copy, the shops Mantle has on it are assigned it, and no other shop is offered it.
- "No plan" is the default and a shop with no subscription reports no features.

## 2026-09-23 — Sell on Shopify from Paywall, on either billing system

Release ID: 2026-09-23-shopify-legacy-billing

Paywall now holds the plans, prices and trials for Shopify apps that bill through the Billing API, including a different price per Shopify plan tier.

- Each Shopify app chooses how new subscriptions are created: Shopify App Pricing, or the Billing API. Your app asks Paywall what to do and gets either Shopify's hosted plan page or the exact charge to create, so switching an app between the two changes nothing in your code.
- A plan can cost a different amount on each Shopify plan tier — Basic, Shopify, Advanced, Plus and development stores — with one price covering every tier you do not single out. Annual prices sit alongside monthly ones.
- Plans are public, or unlisted and offered only to the shops you assign. Tick "show this plan alone" and the shops on it see only that plan; a per-shop setting overrides it where one merchant has to differ.
- A shop can have its own negotiated price and trial on a plan, so a private deal survives a price change. An unlisted plan can be assigned to a new shop at any time.
- Development and partner stores are always charged in test mode, and a trial is offered only to a shop that has never subscribed to the app.
- None of this grants access: a shop's plan still comes from the subscription Shopify reports, so prices and visibility can change without moving anyone's features.
- The plan lands the moment a merchant approves a charge. Shopify's Partner API cannot show Paywall a Billing API charge until its feed catches up about an hour later, so your app now passes the subscription it reads with its own Admin token to refreshShopifyShop, and Paywall grants the plan on that call. The feed takes over when it arrives, and a manual grant still wins.

### Integration changes

Breaking change: no. SDK revision: 2026-09-23.1.

1. Update hub-server.js. On the app's Shopify page, pick how new subscriptions are created; on each plan, set its Shopify prices, visibility and trial, then publish.
2. In your app, call hub.shopifySubscribe({ shop, shopPlan, planKey, returnUrl }) where the merchant chooses a plan. For mode "app_pricing" redirect to the url; for "billing_api" pass the charge to appSubscriptionCreate and redirect to its confirmationUrl. Paywall never calls Shopify: creating a charge needs the shop's Admin token, which stays in your app.
3. Pass shopPlan (shop.plan.displayName from the Admin API) so tier prices apply; without it every plan falls back to its tier-less price. Render plan cards from hub.shopifyPlans({ shop, shopPlan }).
4. After the merchant approves, call hub.refreshShopifyShop() so the new plan is read back immediately. On the Billing API, pass subscription: what you read from currentAppInstallation.activeSubscriptions (name, status, test, amount, currency, interval) — Shopify's Partner API cannot see that charge for about an hour, and the charge name is what Paywall maps to a plan.

### Verify in your app

- A Basic store and an Advanced store see their own prices for the same plan; a store on a tier you did not single out sees the fallback price.
- A shop assigned an unlisted plan sees it; no other shop does, and subscribing to it from another shop is refused.
- A development store's charge comes back with test true. A shop that has subscribed before gets no trial.
- Switching the app between App Pricing and the Billing API changes what the same call returns, and no shop's plan or features move.
- A merchant who approves a Billing API charge is on the new plan when the page reloads, not an hour later; a charge name no plan maps to grants nothing, and a shop with a manual grant keeps it.

## 2026-09-23 — Prevent repeat payments for the same resource

Release ID: 2026-09-23-single-resource-checkout

Apps that sell one form or other resource per customer identity can reuse an open checkout and stop another purchase after payment.

- For apps with single-purchase protection enabled, simultaneous requests share one checkout. Already-paid customers return to the app confirmation page.
- Older open Stripe checkout links are expired before a replacement is created. Unknown payment states ask the customer to retry or contact support before paying again.
- Protection supports one-time Stripe payments. Recurring, simulated, other-provider and free orders are rejected for protected apps; other apps keep their existing purchase behavior.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Ask your Paywall operator to enable single-purchase protection only when each customer identity represents one independently purchased resource. A new resource must have a new identity.
2. No SDK upgrade is needed. Consumer apps should check access before displaying checkout, keep the resource in the return URL, and offer payment-status retry and support when confirmation is delayed.

### Verify in your app

- Start checkout for the same resource in two tabs and confirm only one Stripe session is created. Pay once, then revisit checkout and verify it returns to the existing resource.
- Verify a different resource can still be purchased, and an unavailable payment check cannot start another charge.

## 2026-09-20 — Find every Mantle plan your shops are on

Release ID: 2026-09-20-mantle-private-plans

Import from Mantle now sees Custom and Hidden plans, not only the ones offered to the first shop it asks.

- Mantle offers a Custom plan ("available by customer") only to the shops it was assigned to, and a Hidden plan may be offered to nobody at all. Connect and preview now reads every shop, a batch at a time, instead of a sample — so those plans appear with their feature values, ready to create and map, however many shops the app has.
- The feature flags only a Custom or Hidden plan uses are found the same way, so the shops on those plans keep the same flags after the move.
- The shop run reports every Mantle plan the shops are on or are offered, how many shops each one has, and the Paywall plan its name maps to.
- Plans shops are on that map to nothing are called out, so no shop is left on the default plan because its plan was never on screen.
- Archived Mantle plans nobody is on never appear, and nothing is created or mapped without you choosing it.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. On the app's Shopify page, open Import from Mantle and choose Connect and preview again. It reads the shops in batches; wait for "Read all N shops" before copying, since that is when the plan list is complete.
2. Create and map any Custom or Hidden plan that now appears, then publish. Run Preview shops and check the unmapped-plans note before importing extras.

### Verify in your app

- A shop on a Mantle plan offered to no one else appears in the preview with its feature values, even when it is the last shop read.
- The shop preview lists that plan with the number of shops on it and no Paywall plan; after creating and mapping it, the note clears and those shops report a matching plan.

## 2026-09-18 — Keep imported Mantle extras active

Release ID: 2026-09-18-mantle-import-override-expiry

Imported customer extras no longer inherit an old Paywall override's expiry date.

- When a Mantle extra raises a customer's current access, importing it replaces the lower override and clears its old expiry. Extras skipped because the customer already has equal or higher access leave existing overrides unchanged.
- Imported extras have no expiry; you can set one afterwards on the customer page or through the admin API.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. Preview and rerun an affected Mantle customer import, then verify the customer's effective features.

### Verify in your app

- Import a higher Mantle extra over an expired override and over a future-dated lower override. Confirm the new value applies and has no expiry. Import again and confirm no additional overrides are written.

## 2026-09-17 — Payment alerts with customer context

Release ID: 2026-09-17-richer-payment-alerts

See who paid, what they bought, and where to find their customer record in a clearer, more readable notification.

- New payment alerts include the amount, plan, customer name and email, country when available, provider, recorded time, customer and payment references, and a customer-dashboard link. Before-discount totals and discounts appear when recorded and reconciled with the paid amount.
- Telegram uses a bold heading, compact detail rows and a small event emoji. Test payments are clearly labeled; free checkouts and Shopify earnings retain distinct descriptions. Topic routing is preserved.
- Future verified Stripe checkout payments retain the supplied customer name, email and billing country for alerts. Razorpay retains the supplied customer name and email when available. Existing customer profile details provide a fallback, with profile country labeled separately from billing country.
- Missing optional details are omitted. No country is inferred from currency. Previously sent notifications are not resent or edited, and already queued alerts retain their saved details.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Existing enabled notification rules automatically use the richer details for newly collected payments. No SDK update or Telegram re-pairing is required.
2. For profile fallbacks, pass properties.name and a two-letter properties.country code through the existing server identify call, or update the customer properties in Paywall. Stripe billing details take precedence when available; an SDK update alone does not populate missing customer data.
3. Customer details are included in the organization's selected notification destinations, including email and Slack. Recurring purchase and notification-event coverage are unchanged.

### Verify in your app

- Complete a test payment matched by a test-mode rule and verify the test label, amount, plan, supplied customer details and customer link. Verify the link requires dashboard access.
- Check a payment without customer details, a discounted payment, a free checkout and a Shopify earning. Confirm missing details are omitted and event descriptions remain accurate.
- Verify special characters in customer names display as text, delivery stays in the selected Telegram topic, and replaying a webhook does not send a duplicate alert.

## 2026-09-16 — Send Telegram alerts to the topic you connect

Release ID: 2026-09-16-telegram-topic-routing

Pairing the Paywall bot inside a Telegram topic now keeps the confirmation reply and alerts in that topic instead of General.

- Paywall remembers the topic selected by your pairing command and shows its number before confirmation and in destination and rule labels.
- Topics in the same group are separate destinations. Reconnecting the same topic preserves its rule selections; existing connections without a topic keep their previous routing.
- If Telegram rejects a topic delivery, Paywall records the failure without retrying in General.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. In Organization → Notifications, start a new group connection and send the new pairing command inside the desired Telegram topic. Cancel any older pending connection first.
2. Confirm the group and topic number in Paywall, send a test, then edit each applicable rule under Send to: select the new topic destination, deselect the old group destination, and save. Pairing alone does not move existing rules or enable live alerts.
3. Existing destinations cannot recover a previously discarded topic; they must be paired again. Custom-bot setup remains chat-only. Telegram channels remain unsupported by Paywall bot pairing.

### Verify in your app

- Pair inside a topic and verify both the pairing reply and a test arrive there. Select it in an enabled rule and verify the next matching event uses that topic.
- Connect two topics in one group and verify they remain distinct; reconnect one and verify its destination and rule selections are preserved.
- Verify private chats and General still work, and a rejected topic send does not fall back to General.

## 2026-09-16 — Choose when Stripe customers are created

Release ID: 2026-09-16-stripe-customer-timing

Keep Stripe’s regular flow, create customers when checkout starts, or create them in the background when they are identified.

- App Settings now offers three Stripe customer creation options. Existing apps keep Stripe default, where one-time payments can remain guest payments.
- Checkout and identification modes reuse customers created by Paywall for the same app customer, Stripe connection, account and test/live mode. Matching emails never merge purchase accounts.
- Identification with a valid email starts background creation without waiting for Stripe. New browser checkout accounts and hosted email sign-in use the same setting. Checkout creates a missing customer if background creation has not completed.
- Saved provider references stay separate from your app’s custom customer properties; updating those properties preserves the payment connection.
- The first checkout can take longer while a customer is created. Stripe customer creation never grants access or proves payment. This setting does not add recurring-payment lifecycle support or import existing Stripe customers.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Open App Settings → Stripe customers and save a creation option. It applies to future requests immediately; no publish or SDK update is required.
2. Alternatively PATCH /v1/admin/apps/:app with {stripeCustomerCreation: 'stripe_default' | 'checkout' | 'identify'} using an app admin token or master key.
3. For identification mode, send a valid email with the existing server identify call. Browser checkout and hosted email sign-in already provide one. Configure the selected Stripe connection for the app’s active test/live mode.
4. An identify response does not guarantee that Stripe creation has finished. Background failures appear in the Webhooks event log and can retry on a later identification or checkout. Previously unresolved attempts may require reconciliation by the app owner.

### Verify in your app

- In Stripe test mode, identify a customer and check the selected timing; repeat checkout and verify the same Stripe customer is reused.
- Verify default mode retains guest payment behavior; failed payment and customer creation alone must not grant paid access.
- Verify concurrent identification and checkout do not create separate customers. Test connection changes, key rotation, matching emails on distinct identities, and test/live separation.

## 2026-09-16 — See when notification rules need attention

Release ID: 2026-09-16-notification-rule-guidance

Notifications now distinguish a connected destination from a destination selected for live alerts, with clear next steps when a rule cannot send.

- Warnings identify enabled rules with unavailable destinations and connected destinations that are not selected in any enabled live rule. Edit the affected rule directly from its warning.
- The setup checklist and test confirmation explain that tests bypass rules. Enabled rules must select at least one connected, enabled destination before they can be saved.
- Telegram destinations distinguish groups from personal chats. Older custom-bot destinations with an unknown chat type are labeled explicitly instead of guessing.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK or consumer-app changes are required. In Organization → Notifications, review warnings, select the intended destination under Rules → Send to, and save. Connecting a different chat does not move existing rules; missed historical events are not resent.

### Verify in your app

- Connect a destination and send a test before selecting it in a rule; the page must still say that no live alerts are configured for it.
- Pause or disconnect a rule's only destination and confirm the warning. Select an available destination, save, reload, and verify the warning clears. Confirm the next matching live event reaches the selected destination.
- Check group and personal-chat labels in the destination list, pairing confirmation, and rule recipients.

## 2026-09-15 — Hand a Shopify app to your AI agent

Release ID: 2026-09-15-shopify-agent-prompt

The app's Shopify page now has a ready-made prompt for connecting a Shopify app's code to Paywall, and the integration guide covers testing and moving from Mantle.

- New last section on the app's Shopify page: a copyable prompt for Claude Code, Codex or Cursor, filled in with this app's Paywall app and Shopify app handle.
- The integration guide's Shopify section adds working offline from the last known plan, the app handle per environment, a debug switch, a testing checklist, the steps for moving from Mantle, and common pitfalls.
- A Billing API subscriber who switches to an App Pricing plan is never counted as a cancellation, even when Shopify reports the old charge's cancellation late.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK change. On the app's Shopify page, copy the prompt from "Connect the app's code with your AI agent" and paste it into your coding agent in the Shopify app's repository, together with the agent token from the app's Overview page.

### Verify in your app

- The agent's debug output shows the shop's plan and features after a plan is chosen on Shopify's plan page.
- Reports show a plan switch from the Billing API to App Pricing as one upgrade or downgrade, with no cancellation.

## 2026-09-15 — Refresh a Shopify shop the moment it returns from the plan picker

Release ID: 2026-09-15-shopify-shop-refresh

Shopify apps can re-read one shop's subscription on demand instead of waiting for the next scheduled sync.

- Your backend can refresh one shop right after Shopify's plan picker redirects back. It checks the shop's current Shopify subscription, so the new plan and features are there on the first call.
- If Shopify cannot be reached, the refresh still answers with the shop's last known plan, so your app never has to handle an error there.
- A plan you grant by hand on the customer page always wins over Shopify's data. Clearing it puts the shop back on its Shopify plan straight away.
- Customer pages for shops gain a Refresh from Shopify button.
- For plans that come from Shopify, identify and entitlement responses include the shop's Shopify subscription: plan handle, price, billing interval, trial end and any scheduled cancellation.

### Integration changes

Breaking change: no. SDK revision: 2026-09-15.1.

1. Update hub-server.js and call hub.refreshShopifyShop({ shop, shopId }) on your backend when the plan picker returns. Pass the lower-cased myshopify domain as shop and gid://shopify/Shop/N as shopId. Custom wrappers call POST /v1/shopify/refresh with the secret key.
2. Re-run identify afterwards so the frontend receives a token for the new plan. Never trust the plan_handle query parameter itself.
3. Treat refreshed: false as showing the last known plan; the reason field says why. One shop refreshes at most once per 10 seconds, so do not poll it.
4. Identify shops with the lower-cased myshopify domain as userId so identify, manual grants and Shopify data share one customer.
5. Read the new shopify field in identify or entitlement responses for trial notices and purchase tracking. It is only present when the plan comes from Shopify.
6. To block every gated feature for shops without a subscription, keep a default plan with no features. Otherwise the lowest-sorted plan is the default.

### Verify in your app

- Change a development store's plan, call the refresh and confirm the new plan and features come back.
- Call the refresh twice in a row and confirm the second answers with reason cooldown and the same plan.
- Grant a plan by hand, run a sync and confirm it is kept. Clear it and confirm the shop is back on its Shopify plan.
- Read an unknown shop and confirm it gets the default plan.

## 2026-09-14 — More efficient fresh access checks

Release ID: 2026-09-14-fresh-access-checks

Fresh session and access checks retrieve the customer's plan and individual feature grants together, reducing waiting after payment.

- Fresh checks continue to read current access, including revoked access and changed customer feature grants. Default-plan selection and cached sessions retain their behavior.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No integration changes or SDK update are required. Continue refreshing with fresh: true after payment, or using the fresh entitlement endpoint.

### Verify in your app

- Complete a test payment and confirm fresh access through browser and server integrations.
- Revoke access or change an individual feature grant and confirm a fresh check reflects the change.
- Check a customer without a paid plan and confirm the default plan and individual feature grants are retained.

## 2026-09-14 — Less waiting when opening checkout

Release ID: 2026-09-14-checkout-data-loading

Checkout reuses the app and plan it has already loaded and retrieves pricing, provider settings and discounts together.

- Browser, server and hosted billing checkouts perform less repeated work. Current prices, quote confirmation, provider availability and discount limits remain enforced.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No integration changes or SDK update are required. Existing checkout arguments and responses are unchanged.

### Verify in your app

- Complete a test checkout with and without expectedQuote and confirm the amount and paid access.
- Change a price or disable a provider after displaying a quote; checkout must request fresh confirmation.
- Verify explicit and automatic discounts, redemption limits and free orders. Existing recurring purchase restrictions remain unchanged.

## 2026-09-14 — More efficient customer requests

Release ID: 2026-09-14-client-request-efficiency

Customer session, access, checkout and billing-link requests do less repeated work while preserving the same integration behavior.

- Customer requests now resolve the customer and app together. Existing customer tokens, payment confirmation and access checks keep their behavior.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No integration changes or SDK update are required. Continue using the existing Client API endpoints.

### Verify in your app

- Refresh a session, complete a test checkout and confirm fresh paid access.
- Rotate a customer token and confirm the previous token is rejected. A different customer must not read the checkout result.

## 2026-09-14 — Import from Mantle in the dashboard

Release ID: 2026-09-14-import-from-mantle

Apps moving from Mantle can bring over plan feature values and per-shop extras with the app's Mantle API key, from the app's Shopify page.

- The app's Shopify page has a new "Import from Mantle (one-time)" section below Plan mapping.
- Paste the app's Mantle App ID and API key to see Mantle's plans next to the Paywall plans they map to, with every feature value that would change. It works on every Mantle plan.
- Set a new app up from Mantle alone: create its plans, the feature flags you tick (typed as in Mantle) and a "No plan" default, copy Mantle's values in, and map Mantle's plan names. The result is a draft until you publish.
- Preview every shop Paywall knows against Mantle (plan matches, plan differs, not mapped, no subscription, not in Mantle), then import the extras Mantle gave individual shops as feature overrides.
- The import only ever adds access: an extra a shop already matches or beats in Paywall — through the plan it is on, whoever granted it, or an override it already has — is listed as skipped instead of written.
- Paywall only reads from Mantle. The key is used for each request only and never stored. The same actions are available through the admin API.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK change. Run the Batch import first so Paywall knows your shops. In Mantle, open your app's API keys and copy the App ID and API key.
2. On the app's Shopify page, open Import from Mantle, paste both and choose Connect and preview.
3. Check the Paywall plan chosen for each Mantle plan (or "Create plan"), tick the feature flags your app checks, create and copy, then publish.
4. Preview the shops, review the report, then import the extras. Delete the API key in Mantle when you are done.

### Verify in your app

- The plan preview lists each Mantle plan with the values that would change.
- After copying and publishing, the Paywall plans carry Mantle's values.
- The shop preview changes nothing; the import adds overrides only for extras above the shop's Mantle plan and above what the shop already has in Paywall.

## 2026-09-13 — Keep existing Shopify subscribers when you move to App Pricing

Release ID: 2026-09-13-shopify-billing-api-alongside-app-pricing

One app can now serve merchants still billed through the Billing API and new App Pricing subscribers at the same time.

- Link settings gain "Also read Billing API subscriptions". Existing subscribers keep their plan and features, matched by charge name, so every price variant of a plan maps to the same plan.
- When an existing subscriber picks a plan on Shopify's App Pricing page, Paywall treats it as a plan change, not a cancellation.
- Shopify also lists App Pricing subscriptions in the Billing API feed. Paywall recognises those copies and counts each subscription once.
- Shopify's Billing API feed can run about an hour behind. Every sync re-reads its last day, so late changes are still picked up.
- Plan mapping ignores capital letters, so mapping "growth" also covers a charge named "Growth".
- For rehearsals on development stores, "Honour test charges" lets test charges grant plans. It only works while the app is in test mode and is hidden on live apps. Reports keep ignoring test charges.
- A new admin API import brings over per-customer extras from Mantle and reports, shop by shop, whether the plans agree.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No SDK change. On the app's Shopify page, turn on "Also read Billing API subscriptions", then run "Import full history" so older subscriptions are found.
2. Map each Billing API charge name to a plan in the plan mapping. The page lists every name it has seen.
3. To bring over Mantle extras, POST the export to /v1/admin/apps/:app/shopify/mantle-import with dryRun set to true first, review the report, then run it for real.
4. Only turn on "Honour test charges" for apps you rehearse on with development stores. Put the app in test mode first; switching it back to live turns the grants off.

### Verify in your app

- An existing Billing API subscriber keeps the mapped plan after the sync.
- A new subscriber on Shopify's plan page gets their plan, and an existing subscriber who switches there is recorded as a plan change.
- A dry-run import changes nothing and lists every shop with its status.

## 2026-09-11 — Recover browser purchases with email sign-in

Release ID: 2026-09-11-hosted-customer-email

Customers can sign in with an email code, recover purchases on another device, and manage billing from a hosted dashboard.

- Enable Hosted email in app Settings and allow your app's return origins. Customers verify an eight-digit email code to view their browser purchases.
- Set a customer-facing name for hosted screens and the email sender, or leave it blank to use the app name. The sending address stays accounts@paywallhq.com. Browser apps can read getClientSettings() to show sign-in only when enabled.
- Recover one purchase account at a time without merging matching emails or changing customers managed by your existing server login.
- Manage Stripe subscriptions through provider confirmation screens: cancel at period end or change to a supported configured recurring price. Existing subscription access refreshes from Stripe.
- Razorpay and simulated purchases remain one-time payments. Plan changes require single-item Stripe subscriptions and unambiguous configured Stripe price IDs; inline prices can be canceled but are not plan-change targets.

### Integration changes

Breaking change: no. SDK revision: 2026-09-11.4.

1. Update hub-client.js and hub-react.js if used. Existing server integrations keep identify, customer tokens, and their own login.
2. Publish your catalog, enable Hosted email, and add the exact HTTPS origin of each static app. Choose a customer-facing name or leave it blank for the app name. Call getClientSettings() to render sign-in; use { force: true } to refresh settings. Keep Browser checkout enabled to accept new customers.
3. Initialize guest: true, add a Sign in / Recover purchases button calling signIn({ returnUrl }), and await completeSignIn() on the callback before rendering access or analytics. The SDK uses sessionStorage, localStorage, state, and S256 PKCE; return in the same tab.
4. Use the hosted dashboard to choose a purchase account and manage billing. Recovering rotates its long-lived customer token; previously issued entitlement JWTs can remain valid for up to 15 minutes.
5. Ensure Stripe webhooks include customer.subscription.updated as well as checkout.session.completed and customer.subscription.deleted. Configure matching provider price IDs for supported plan changes; Stripe shows final charges and confirmation.

### Verify in your app

- Verify an email code, choose a paid browser account, return to the static app, and confirm paid features survive reload.
- Reject wrong, expired, replayed, and cross-app codes. Reject an unapproved callback origin. Matching server-managed customers must remain inaccessible through hosted email.
- Verify cancellation retains access until period end, a supported plan change updates access, and a newer purchase or manual grant is preserved.

## 2026-09-11 — Pair Telegram with a visible code

Release ID: 2026-09-11-telegram-pairing-code

Copy a pairing command from Paywall to connect your personal Telegram chat or team group.

- New Telegram connections show a temporary pairing code and a copyable command in Notifications.
- Send the command to @PayWallHqBot privately or in a group you administer, then confirm the chat name in Paywall. Codes expire after 15 minutes and connect only one chat.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. In Organization → Notifications, start a Telegram connection and copy the pairing command.
2. Send the command in your chosen chat, return to Paywall and confirm. After reloading the page, cancel and restart if you need to display a new code. Existing destinations and SDK integrations need no changes.

### Verify in your app

- Check the matched chat name, confirm the connection and send a test notification.
- Sending /start without a code does not connect a chat. Group pairing requires a group admin.

## 2026-09-11 — Accept payments from a static app

Release ID: 2026-09-11-browser-checkout

Customers can enter an email and open hosted checkout without your app running a backend.

- Enable Browser checkout in app Settings, then use your publishable key and the browser SDK to start checkout.
- The SDK saves a new customer credential before payment and restores access in the same browser after returning or reloading.
- Entering an email never signs into an existing customer or retrieves their purchases. Unverified guest emails cannot qualify for email-targeted offers; existing server customer IDs keep their behavior regardless of prefix.
- Email sign-in, recovery across browsers, and a subscription-management dashboard are deferred. Clearing browser storage loses guest access; browser feature checks can be modified by the browser user.

### Integration changes

Breaking change: no. SDK revision: 2026-09-11.2.

1. Update hub-client.js and, for React apps, hub-react.js from the SDK manifest. Existing authenticated integrations can keep their current setup.
2. Publish the app catalog and enable Browser checkout in Settings. Initialize createHubClient with baseUrl, publishableKey and guest: true; add an email field and pass email to checkout. Never include a secret key.
3. For React, pass guest to PaywallProvider and email to useCheckout. Use HTTPS and a browser with Web Locks and working localStorage. Return to the same app origin and preserve its saved credential.
4. Read checkoutResult after return and call refreshSession({fresh: true}) to load paid access. Follow the static app example in the integration guide. This adds checkout, not new recurring-payment lifecycle support.

### Verify in your app

- Complete a test checkout, confirm paid features appear, then reload and confirm access remains.
- Verify blocked storage prevents checkout. In a separate browser, entering the same email creates a new customer without existing paid access.
- Verify declining payment grants nothing and fresh sessions reflect revoked access. Guest emails must not qualify for email-restricted codes or automatic offers.

## 2026-09-11 — Feature overrides can expire

Release ID: 2026-09-11-override-expiry

Give a customer temporary access or a higher limit that ends on its own.

- Set an expiry date on any per-customer feature override from the customer page or the admin API.
- An expired override stops applying on the next read, including sessions already cached. Session tokens never outlive it.
- Expired overrides stay visible on the customer page, marked expired, and no longer count as overrides in customer lists.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. No integration change is needed. Overrides without an expiry keep working as before.
2. Admin API clients can send expiresAt as an ISO date or null to PUT /v1/admin/apps/:app/customers/:user/features/:feature. Omitting it keeps the current expiry.
3. An override replaces the plan's value for that feature, so it can raise or lower a limit.

### Verify in your app

- Set an override with a past expiry and confirm the plan's value applies.
- Set one with a future expiry and confirm the override applies until then.

## 2026-09-11 — Start with a prompt for your coding agent

Release ID: 2026-09-11-agent-first-setup

New apps open with a complete agent prompt and an access key valid for two days.

- The app overview leads with a copyable prompt containing the app identity, integration guide, and scoped access key instead of a manual setup checklist.
- New apps receive a two-day agent key automatically. Extend the same key when your agent needs more time, including after expiry, or revoke it to stop access.
- Agent keys cannot create, extend or revoke keys. Revoked keys cannot be renewed.

### Integration changes

Breaking change: yes. SDK revision: unchanged.

1. Open the app overview and choose Copy agent prompt. Existing apps without an active key can generate one there; existing keys keep their current expiry.
2. Use Extend access to add time to the current expiry, or from now if expired. Your agent keeps the same key. No consumer SDK update is needed.
3. Admin API behavior change: POST /v1/admin/apps/:app/tokens now defaults to two days when ttlDays is omitted. Automation that intentionally needs a non-expiring key must explicitly send ttlDays: null.
4. Master-key API clients can PATCH /v1/admin/apps/:app/tokens/:id with days set to 1, 2, 7, 30 or 60 to extend an expiring, unrevoked key. Keep the master key out of agent prompts.

### Verify in your app

- Create an app and confirm the prompt includes its app ID, guide URL and a key expiring in two days; paste it into your coding agent.
- Extend an active or expired key and confirm it works with the same value. Revocation stops access and prevents renewal.
- Check that an app key cannot access another app or manage tokens. Test existing provisioning scripts with an explicit ttlDays where needed.

## 2026-09-11 — Faster paywall prices

Release ID: 2026-09-11-faster-price-quotes

Paywall prices stay cached for seven days and refresh automatically when you change pricing settings.

- Price changes refresh cached prices automatically. Updates may take 60 seconds or more to appear on your paywall.
- Use Refresh cached prices on the Plans page or beside a plan’s prices to rebuild them manually. This keeps the next visitor’s quote fast.
- Background maintenance renews older price snapshots to keep quiet apps ready for their next visitor.
- Checkout still validates current pricing and provider availability. Public quotes remain list prices, excluding customer-specific discounts.
- Custom integrations can pass expectedQuote to detect a changed price before opening payment. A QUOTE_CHANGED response includes the updated quote for customer review.

### Integration changes

Breaking change: no. SDK revision: 2026-09-11.1.

1. Existing quote and checkout requests keep working without an SDK update. Prices, plans, provider settings, app mode, imports, and publishing refresh cached prices automatically. Existing apps are loaded on their first request if no snapshot exists.
2. For a manual refresh, use Refresh cached prices in Plans, or POST /v1/admin/apps/:app/quote-cache/refresh with an authorized app admin token. Refresh replaces the snapshot; propagation may take 60 seconds or more.
3. To detect price changes in your own checkout UI, update the SDK files you use and pass the displayed single-plan quote as expectedQuote through hub.checkout() or useCheckout(). Custom backend wrappers must forward it too.
4. On error.code QUOTE_CHANGED (HTTP 409), display error.quote and ask the customer to continue before retrying with that updated expectedQuote. Do not automatically retry or continue when available is false. API clients read code and quote from the response body.
5. The comparison covers the list price, currency, interval, provider, and mode. Discounts are still resolved separately at checkout. Existing integrations without expectedQuote use current checkout pricing; the hosted billing portal continues its existing checkout flow.

### Verify in your app

- Repeated quotes keep the same prices and respect country/currency/provider selection.
- After editing a price or refreshing manually, the displayed quote updates after propagation and stays fast past one minute.
- A checkout with an old expectedQuote returns QUOTE_CHANGED before opening payment; confirming the returned quote allows checkout at the current price.
- Existing checkout calls and discount codes continue to work without expectedQuote.

## 2026-09-09 — Connect your team to the Paywall Telegram bot

Release ID: 2026-09-09-telegram-connect

Connect a personal chat or team group to @PayWallHqBot without copying a bot token or Chat ID.

- Organization owners and admins can connect Telegram with a temporary link, then confirm the selected chat in Paywall.
- Each organization chooses its own destinations, apps and notification events. Group connections require a Telegram group admin.
- Use your own bot if preferred. Pause, test or disconnect destinations from Notifications.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. In Organization → Notifications, choose your personal chat or a group and select Connect with Paywall bot.
2. Open Telegram, start the bot or add it to your group, then return to Paywall and confirm the chat within 15 minutes.
3. Send a test and create an enabled rule for your apps and events. No SDK or consumer-app changes are required; existing custom bots continue to work.

### Verify in your app

- Confirm the correct chat name and receive a test message before enabling a rule.
- Check delivery history for a selected billing event. Pausing or disconnecting stops future sends; messages already being sent cannot be recalled.
- Only existing supported billing events trigger alerts. This does not add recurring Stripe invoice or Razorpay subscription ingestion; Shopify events still depend on feed polling.

## 2026-09-08 — Automatic discounts for the right customers

Release ID: 2026-09-08-automatic-discounts

Apply an offer automatically to everyone or to customers matching your email list, countries and schedule.

- Choose automatic offers or customer-entered codes in App → Discounts.
- Combine email, country, plan and time restrictions. The largest eligible automatic offer wins; offers do not stack.
- The hosted portal previews eligible offers. Existing checkout calls apply them without a code, including 100% free orders.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Create an automatic offer in App → Discounts. No SDK update is required; omit discountCode to let Paywall select an offer.
2. Use the authenticated customer’s email. Server callers supply a trusted country; browser and portal checkouts use detected country. See /integrate.md#automatic-discounts.
3. Use checkout amounts for reporting; public quotes remain list prices. Entered codes override automatic selection. One-time purchases only, with separate live/test limits and one reservation per customer per offer.

### Verify in your app

- Test matching and non-matching emails and countries, and scheduled start/end times.
- Check that the largest eligible offer applies, that codes override it, and that exhausted offers stop applying.
- Confirm a free checkout grants access and a repeat resumes the same checkout without restoring revoked access.

## 2026-09-08 — Share an update with your coding agent

Release ID: 2026-09-08-changelog-agent-copy

Copy a changelog entry as a ready-to-paste prompt for your coding agent.

- Changelog dates now use a readable month, day and year.
- Each entry has a Copy for coding agent button with its changes, adoption steps, verification checklist and guide links.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Open the changelog, copy the relevant entry and paste it into your coding agent. No SDK update is needed for this improvement.

### Verify in your app

- The copied prompt includes the selected release and its integration instructions.

## 2026-09-08 — Discount codes, managed in Paywall

Release ID: 2026-09-08-app-discounts

Create a discount once for your app, including private 100% codes for testing your live checkout without a charge.

- Create percentage codes with plan and customer restrictions, expiry, and usage limits in App → Discounts.
- Partial discounts charge the reduced amount through Stripe or Razorpay. A 100% discount completes a free order and grants access without contacting a payment provider.
- The hosted billing portal now accepts codes for one-time purchases. Subscription discounts are not supported yet.
- Checkout results preserve the discounted total even if the catalog price changes later. Free-order retries do not restore access that has been revoked.
- The public changelog, SDK manifest, and upgrade guide now help integration agents find and adopt new features.

### Integration changes

Breaking change: no. SDK revision: 2026-09-08.1.

1. Existing checkouts continue to work without changes. SDK updates are needed only to use discount codes in a custom checkout integration; the hosted portal already includes the field.
2. Compare your vendored SDK files against /vendor/manifest.json. Download only the files your app uses, verify their SHA-256 hashes, and review the diff before replacing local copies. Preserve local customizations.
3. Add an optional discount-code input and pass discountCode through hub.checkout() or useCheckout(). Backend callers also pass their existing authenticated userId. Direct API clients send discountCode to POST /v1/checkout or POST /v1/client/checkout.
4. Continue redirecting to result.url, including for 100% discounts. Read the Paywall checkout result and fresh entitlement after return; do not assume every order opens Stripe or Razorpay.
5. Use amountSubunits from the checkout result for purchase reporting. A free order has provider discount and amount zero; do not report the original list price as revenue.
6. For a private live-site test, create a 100% code restricted to an existing customer's external ID with a maximum of one checkout. Starting checkout reserves the use, including abandoned attempts. Test and live caps are separate.

### Verify in your app

- A checkout without a code still uses normal pricing.
- A partial code shows the reduced amount at the provider and in the completed checkout result.
- A restricted 100% code grants the right plan, returns to the app, reports zero paid, and keeps access after refresh.
- Invalid codes and codes restricted to another account fail without granting access.

## 2026-09-08 — Clearer saved payment settings

Release ID: 2026-09-08-saved-provider-keys

See whether your payment keys are saved without exposing the keys themselves.

- App and organization provider settings now distinguish saved keys from missing configuration.
- Separate add, replace and delete actions make it clearer what will change.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Open Providers to review each mode. Use Verify to check that saved keys work; no SDK update is needed.

### Verify in your app

- Saved keys remain marked as saved after reloading settings.

## 2026-09-08 — Recover a payment connection

Release ID: 2026-09-08-provider-recovery

Restore a connection when its saved payment credentials can no longer be read.

- Provider settings show when a connection needs recovery and let an administrator enter replacement keys.
- Recovery keeps apps and payment records. Both unreadable credential sets are replaced, so each mode and its webhook secret must be configured again.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Use Recover connection in organization provider settings, then configure and verify each mode you use. No SDK update is needed.

### Verify in your app

- The recovered connection passes verification and linked apps no longer show a recovery warning.

## 2026-09-08 — Shopify subscriptions and reports

Release ID: 2026-09-08-shopify-reporting

Connect Shopify billing activity to customer access and reporting in Paywall.

- Link a Shopify Partner app, map its subscription plans to Paywall plans, and sync customer activity.
- View app and organization reports. Development-store subscriptions are excluded from reports.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Connect Shopify in the dashboard and map its plans to Paywall plans.
2. Identify customers by their permanent myshopify domain and use the existing entitlement and feature checks. Send upgrades to Shopify’s plan picker.
3. Allow up to ten minutes for billing changes to appear, or use Sync now while testing. No new SDK method is required.

### Verify in your app

- After a sync, a shop on a mapped paid plan receives the expected features.

## 2026-09-07 — Move apps between organizations

Release ID: 2026-09-07-move-apps

Reorganize your apps from App Settings.

- Owners and admins can move an app into another organization they also manage.
- The dashboard redirects to the app in its new organization after the move.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Choose the destination in App Settings. Review payment connections after moving, especially when using shared organization providers. No SDK update is needed.

### Verify in your app

- The app appears in the destination organization and its settings open successfully.

## 2026-09-06 — A clearer handoff to your coding agent

Release ID: 2026-09-06-agent-integration-pages

Start an integration with a ready-to-copy prompt and focused product guides.

- New pages explain AI-agent integration and feature flags.
- The homepage provides a copyable integration prompt and clearly labels upcoming frontend paywalls.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Copy the prompt from the homepage or For AI agents page, then supply your app-scoped token to your coding agent. Existing integrations need no changes.

### Verify in your app

- The agent can open the integration guide and identify the app it is authorized to configure.

## 2026-09-06 — Clearer email verification

Release ID: 2026-09-06-email-verification

Follow a clear path from the verification reminder to a confirmed account.

- A verification reminder includes a resend action, and successful verification opens a confirmation page.
- Where verification is required, unverified accounts can browse the dashboard but must verify before making changes.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Follow the verification email or use Resend from the dashboard reminder. Consumer-app integrations need no changes.

### Verify in your app

- The confirmation page opens after verification and the dashboard allows permitted changes.

## 2026-09-06 — Create your own Paywall workspace

Release ID: 2026-09-06-open-signup

New users can register and start setting up their own organization.

- Registration is open by default, with an invitation-only option still available.
- New accounts start with their own empty workspace and do not inherit another organization’s apps.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Use Sign up to create an account and continue into setup. Existing consumer-app integrations need no changes.

### Verify in your app

- A new account reaches onboarding without seeing another organization’s apps.

## 2026-09-06 — Email invitations for your team

Release ID: 2026-09-06-team-email

Invite teammates by email and keep a shareable invitation link available.

- Organization invitations can now be delivered by email when email delivery is available.
- The Members page also provides a link to copy, so invitations can be shared directly.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Owners and admins can invite teammates from the organization’s Members page. Consumer-app integrations need no changes.

### Verify in your app

- An invited teammate can open the invitation, sign in or register, and join the intended organization.

## 2026-09-06 — Adding an app no longer stalls

Release ID: 2026-09-06-existing-org-app-setup

Fixed a silent failure when creating an app inside an existing organization.

- App-only setup no longer requires organization fields that are not shown on the form.
- You can add another app without repeating organization setup.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Use Add app from an existing organization and submit the app details. No SDK update is needed.

### Verify in your app

- Submitting valid app details creates the app instead of leaving the form unchanged.

## 2026-09-06 — Guided workspace setup

Release ID: 2026-09-06-guided-onboarding

Create your organization and first app through one guided flow.

- Setup guides you through organization details and app details before creating your workspace.
- The same flow supports adding an app to an existing organization or starting another organization.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. New accounts can follow onboarding; existing accounts can start setup from the dashboard. No consumer-app changes are needed.

### Verify in your app

- Completing setup opens the new app in the correct organization.

## 2026-09-06 — Personal accounts and organization membership

Release ID: 2026-09-06-team-accounts

Sign in with your own account and work with your team in shared organizations.

- Organization membership controls which apps and dashboard pages each person can access.
- Owners and admins can manage members and invitations, with separate roles for teammates.

### Integration changes

Breaking change: no. SDK revision: unchanged.

1. Sign in with a personal account and accept an invitation to join an existing organization. Consumer apps keep their existing user authentication.

### Verify in your app

- Members can open their organization’s apps and cannot access organizations they have not joined.
