Sell one price across multiple apps (bundles)
A developer-scoped Plan container whose grants fan out across every app you own — create the bundle, sell it, and both apps get entitled.
A Bundle is a developer-scoped Plan container whose grants/limits fan out across multiple apps — one subscription unlocks features in App A and App B. Reach for this once you're running a small portfolio of apps and want a single price to unlock all of them; don't reach for it to avoid pricing a single app (see pricing philosophy).
There is no CLI verb for bundles yet — create one directly against the API with your secret key. Everything below composes routes that already exist for plans/checkout/memberships; a bundle is not a special case.
1. Create the bundle
curl -X POST $VIBEMONETIZE_API_URL/v1/bundles \
-H "Authorization: Bearer $VIBEMONETIZE_SECRET_KEY" -H "Content-Type: application/json" \
-d '{"slug": "everything", "name": "Everything Bundle"}'
# -> { "id": "01H...bundle", "slug": "everything", "name": "Everything Bundle", ... }2. Create a bundle-scoped plan with cross-app grants
Same POST /v1/plans route as an app-scoped plan, except appId is null and bundleId is the bundle's id. Each grant/limit in the array carries its own appId, so one plan can unlock a feature in App A and a different (or the same) feature in App B:
curl -X POST $VIBEMONETIZE_API_URL/v1/plans \
-H "Authorization: Bearer $VIBEMONETIZE_SECRET_KEY" -H "Content-Type: application/json" \
-d '{
"appId": null,
"bundleId": "01H...bundle",
"slug": "bundle-pro",
"name": "Bundle Pro",
"priceCents": 2900,
"currency": "usd",
"interval": "month",
"trialDays": 0,
"grants": [
{ "appId": "01H...appA", "featureSlug": "pro" },
{ "appId": "01H...appB", "featureSlug": "pro" }
],
"limits": []
}'3. Sell it
Checkout a bundle plan by planId only — bundle plans have no single app scope, so the planSlug resolution path (which resolves within one appId) doesn't apply here. The publishable key used for checkout must be an unscoped (developer-wide) key; an app-scoped key gets forbidden.
import { CheckoutButton } from "@vibemonetize/react";
<CheckoutButton planId="01H...bundlepro" successUrl="/welcome" cancelUrl="/pricing">
Upgrade everything
</CheckoutButton>A comp grant works the same way for testing (vibemonetize grant or POST /v1/memberships with the bundle plan's id) — no checkout needed to verify the wiring.
4. Both apps see the entitlement
Each app renders its own <MonetizeProvider appId="..."> and fetches its own entitlement token, but they resolve from the same membership — resolveEntitlements (the one place entitlement logic lives) keys every grant by `${appId}:${featureSlug}`, so a bundle plan's grants land in whichever app's token asks for them:
// App A
<Paywall feature="pro"><ProFeature /></Paywall>
// App B (different app, different MonetizeProvider appId, same subscription)
<Paywall feature="pro"><ProFeature /></Paywall>The identity crux: this only works if the same end user resolves to the same membership in both apps. A bare anonId is a per-browser, per-app-domain identity — it will not automatically match across two apps on different domains. Use the verified-email flow (POST /v1/end-users/identify + /verify, or vibemonetize grant --email for testing) so the same endUserId is what both apps ultimately resolve against — see the API reference for the identify/verify routes.