# Updating a Paywall integration

This guide is for an AI coding agent or developer working on a consumer app. Read it when adopting a new Paywall feature or refreshing a vendored SDK. Paywall does not automatically rewrite consumer apps.

## Discover what changed

1. Fetch [the changelog JSON](https://paywallhq.com/changelog.json), or read [the Markdown changelog](https://paywallhq.com/changelog.md). Entries have stable IDs, dates, user-facing changes, breaking-change flags, SDK revisions, adoption steps, and verification cases.
2. Compare release IDs with the last entry adopted by this app. Check every intervening entry, not only the newest one. If no prior revision is recorded, inspect the existing integration before deciding which changes apply.
3. Fetch [the SDK manifest](https://paywallhq.com/vendor/manifest.json). It describes the SDK files served by the current deployment, with a revision, download URLs, byte sizes, and SHA-256 hashes. The manifest's revision is a Paywall vendored-source revision, not an npm version.

## Update the code safely

- Locate the app's actual SDK copies and wrappers; consult its own development instructions and inspect git status first. Download only the SDK files the app uses to temporary files. The React hooks import `./hub-client.js`, so keep compatible copies together.
- Verify each downloaded file against the manifest's SHA-256 hash before editing the app. If hashes disagree, refetch the manifest and files: cached responses can briefly span deployments. Do not replace files while they disagree.
- Compare old and new source. Preserve local customizations, import paths, authenticated identity handling, secrets, return URLs, and the app's existing purchase model. Do not blindly overwrite modified SDK files.
- Follow each relevant changelog entry's integration steps. Updating a file alone does not add a new UI control or pass new parameters through custom wrappers.
- Use [the full integration guide](https://paywallhq.com/integrate.md) for the current API contract. Keep backend secret keys on the backend; browser requests use the existing self-scoped customer token.
- Run the app's relevant tests and the entry's verification cases. Use provider test mode for payment-processing tests. A restricted 100% code can test free fulfillment on a live app, but does not verify a card charge.
- Record the SDK revision, source hashes, adopted release IDs, and validation results in the consumer app's own integration notes or lock file. Open a reviewable change using that repository's workflow. Describe what is local, tested, and deployed separately.

## Adopting Stripe customer creation timing

No SDK files or consumer request formats change. Existing apps keep `stripe_default`.
Select **When checkout starts** or **When identified** in App Settings → Stripe customers,
or PATCH the admin app endpoint with `stripeCustomerCreation` set to `checkout` or `identify`.
Identification mode needs a valid email on the existing identify request. It runs in the
background; checkout completes missing creation before opening Stripe. New browser
checkout accounts and hosted email sign-in also schedule creation.

Verify first and repeated checkout, failed background creation, and concurrent identify
and checkout using Stripe test credentials. Check the app customer, connection and mode
remain separate. Existing Stripe records are not imported or merged by email. Compare
first and returning checkout latency; customer creation and initial account verification
add provider requests. See [the full behavior](https://paywallhq.com/integrate.md#stripe-customer-creation).

## Current discount integration

No Stripe coupon or Razorpay offer needs to be created. Add `discountCode` only when the user supplies one:

```js
const result = await hub.checkout({
  planKey: "pro",
  discountCode: enteredCode.trim() || undefined,
  successUrl: window.location.href,
});
window.location.assign(result.url);
```

Backend SDK calls also provide the existing authenticated `userId`. The React `useCheckout()` hook accepts the same `discountCode` option. Custom backend endpoints must forward it to Paywall; replacing the SDK does not update wrappers automatically.

A 100% code returns a normal Paywall checkout ID and success URL, grants access, and records zero paid. After returning, use the customer's `checkoutResult(checkoutId)` and fresh entitlement. The hosted portal already handles code entry. See the current changelog for eligibility, one-time purchase scope, usage limits, and verification cases.

## Adopting automatic offers

No new SDK method or argument is needed. Create an automatic offer in Paywall and omit `discountCode` from normal checkout calls. Custom wrappers should preserve this omission rather than sending a placeholder code. Read the [automatic discount rules in the integration guide](https://paywallhq.com/integrate.md#automatic-discounts), especially trusted email/country matching and how entered codes take precedence.

Verify a listed email and a non-listed email, matching and non-matching countries, both sides of the start/end window, overlapping offers, and the usage cap. Check the actual returned checkout amount and refreshed entitlement. Public quote prices do not include these customer-specific offers.

## Adopting quote-change confirmation

Seven-day quote caching and automatic refresh on settings changes apply without an SDK update.
Background maintenance renews older snapshots in batches to keep quiet apps warm.
A manual Refresh cached prices action is available in Plans; updates may take 60 seconds
or more to propagate. To detect changes before payment,
update the SDK files your app uses, pass the displayed single-plan quote as `expectedQuote`
through checkout and any custom backend wrappers, and handle `error.code === "QUOTE_CHANGED"`.
Display `error.quote` and require a new customer confirmation before retrying. Stop if the new
quote is unavailable. Never auto-retry a changed quote. The hosted billing portal keeps its
existing flow; this optional guard is for custom quote-based UIs.

Verify an unchanged quote, an edited price, a disabled provider, and normal discounted checkout.
The comparison is against the list price; customer-specific discounts still apply separately.

## Adopting browser checkout

For a static consumer app, update `hub-client.js` (and `hub-react.js` for React)
to revision `2026-09-11.2` or newer, using the manifest checks above. Publish the
catalog and enable **Browser checkout** in app Settings. Configure `guest: true`
with only the publishable key, collect an email, and pass it to `checkout()`.
The SDK persists the customer credential before opening payment. Use HTTPS,
Web Locks and working localStorage, and return to the same origin. See the
[static app walkthrough](https://paywallhq.com/integrate.md#static-apps--checkout-without-your-own-backend-phase-1).

Check the checkout result after return, then call `refreshSession({fresh: true})`.
Test payment, decline, reload, blocked storage and revoked access. Same-email
visitors on another browser get separate customers; email is not authentication
and cannot qualify a guest for email-targeted discounts. Email recovery and the
subscription-management dashboard are deferred. Clearing storage loses access.
Existing authenticated integrations do not need to enable guest mode.

## 2026-09-11.4 — hosted customer email and billing

This opt-in release adds `hub.signIn({ returnUrl })`, `hub.completeSignIn({ url? })`, and React `useEmailSignIn()`. Replace `hub-client.js` and `hub-react.js` together from the SDK manifest. The server SDK banner changes with the release; its API is unchanged. Existing server identify, customer-token, checkout, and recovery integrations need no code changes.

In app Settings enable Hosted email and allow each exact static-app return origin. Add a sign-in button, a callback path served by the static app, and call `completeSignIn()` before using purchase state or analytics. Return in the same tab; retain localStorage and sessionStorage. Use the hosted dashboard to select a browser purchase account, view recent payments, and open Manage billing. Recovery rotates that account's customer token; older JWTs can last up to 15 minutes. Hosted email never claims server-created customer records even when their external IDs begin with `guest_`.

For Stripe plan management, add `customer.subscription.updated` to existing webhook endpoints and configure matching, unambiguous recurring Stripe price IDs. Cancellation is at period end; supported plan changes show Stripe's confirmation and charges. Inline prices are not selectable change targets; multiple-item/scheduled/pending-update subscriptions and cross-currency changes are unsupported. Razorpay and simulated purchases remain one-time. The payment history does not yet include renewal invoices.

Verify email delivery, wrong/expired/reused codes, blocked storage, disallowed return origins, two matching guest records, exclusion of server records, paid access after recovery/reload, and cancellation/plan changes with Stripe test credentials before enabling the feature for customers. See the full Hosted customer email section in the integration guide and `examples/browser-checkout/` for the callback and buttons.


The phase 2 SDK revision is now `2026-09-11.4`. Use `hub.getClientSettings()` or `config.client.hostedEmail.enabled` to show/hide email sign-in. Force a refresh with `{ force: true }` after settings changes. App owners can set a customer-facing name; blank defaults to the app name. Hosted screens and sender display names use it with the fixed `accounts@paywallhq.com` address. No frontend flag can override disabled hosted authentication.

## 2026-09-15.1 — Shopify per-shop refresh

Shopify apps only. Update `hub-server.js` to revision `2026-09-15.1` or newer to use `hub.refreshShopifyShop({ shop, shopId })`, or call
`POST /v1/shopify/refresh` with the secret key from a custom wrapper. Call it on the backend when Shopify's
plan picker redirects back, then mint a new session with identify. Pass the lower-cased myshopify domain as
`shop` and `gid://shopify/Shop/N` as `shopId`. Treat `refreshed: false` as "showing the last known plan":
the response still carries the entitlement. Do not poll it. One shop refreshes at most once per 10 seconds.

Verify a plan change followed by a refresh, an immediate second refresh (`reason: "cooldown"`), and an
unknown shop, which reads the default plan.
