Stripe without a client SDK

Connect your own Stripe account, keep your checkout, import prices and subscriptions, and verify customer access from any backend.

Choose Stripe only when creating an app. You can use CashSDK with an existing Stripe business without shipping a mobile app, browser SDK or extension. Your Stripe account receives your payments. CashSDK's own workspace billing is separate.

No bundle ID, store credentials, offering, browser origin or customer session is required for this backend integration. Existing apps can connect Stripe in Marketplace too, next to their other platforms.

Connect your account#

  1. Open Your App → Web and Stripe → Connect Stripe.
  2. Create a restricted key in Stripe's test mode and connect it as Sandbox. Grant write access to Checkout Sessions, Customers and Customer portal, and read access to Prices, Products, Subscriptions, Invoices, Charges, Refunds, Payment Intents, Disputes and Events. Account read is optional. Without read access to Subscriptions, subscription events cannot be applied.
  3. Copy the webhook URL shown for this connection. In Stripe Workbench → Webhooks, still in test mode, create a Snapshot event destination for Your account and select these events:
text
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.subscription.paused
customer.subscription.resumed
invoice.paid
invoice.payment_failed
invoice.voided
invoice.marked_uncollectible
charge.refunded
charge.dispute.funds_withdrawn
charge.dispute.funds_reinstated
charge.dispute.closed
checkout.session.completed
checkout.session.expired
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
  1. Reconnect with the endpoint's whsec_… signing secret. Validate and activate.
  2. In Products → Import from Stripe, select your prices and the entitlement each grants, for example pro. Prices with unsupported billing models are shown with a reason and are not silently converted.

Use the connection's exact webhook URL, not CashSDK's own billing endpoint. Stripe signs events using the endpoint secret. See Stripe's webhook setup. The Sandbox and Production connections each have their own URL. How CashSDK applies the events is on the Stripe billing page.

Keep your checkout#

Your server authenticates the user and selects a configured Stripe Price ID. Use the same user ID that your apps pass to CashSDK. Never take the customer identity, price or success URL directly from an untrusted request.

ts
const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: configuredPriceId, quantity: 1 }],
  subscription_data: { metadata: { cashsdk_user_id: authenticatedUser.id } },
  success_url: "https://your-app.example/billing/return",
  cancel_url: "https://your-app.example/pricing",
}, { idempotencyKey: persistedCheckoutAttemptId });

For a lifetime product, use mode: "payment" and client_reference_id: authenticatedUser.id instead of subscription_data. Import the price as a non-consumable. CashSDK waits for successful payment, including delayed payment methods, before granting access. A Stripe Payment Link cannot name the user on a subscription, so a subscription sold through one waits until you link its Stripe customer, as described below.

Save the checkout attempt ID on your server before creating the session. Reuse it after a timeout. A successful redirect does not prove payment or grant access.

Authorize access on your backend#

Create a restricted CashSDK key with Customers access in Developers → API Keys: a test key (csk_rk_test_…) while you build, a live key (csk_rk_live_…) in production. The key's mode picks the environment, so the request does not name one. Keep it in a server secret store. Use the authenticated user's ID, then fail closed if the API is unavailable or the entitlement is absent.

ts
const response = await fetch(
  "https://api.cashsdk.com/v1/customers/" +
    encodeURIComponent(authenticatedUser.id) +
    "/entitlements",
  {
    headers: { Authorization: "Bearer " + process.env.CASHSDK_SECRET_KEY },
    cache: "no-store",
    signal: AbortSignal.timeout(10000),
  },
);
if (!response.ok) throw new Error("Access verification unavailable");
const { entitlements } = await response.json();
const isPro = entitlements.some((entry) => entry.identifier === "pro");
if (!isPro) return new Response("Pro is required", { status: 403 });

This endpoint returns currently effective entitlements. Customer sessions and publishable keys are unnecessary when only your backend reads access. The optional Node SDK wraps the same API. A test key asking for environment=Production, or a live key asking for Sandbox, is refused with environment_mismatch.

Existing subscribers and billing management#

Run Sync from Stripe after importing prices. It imports subscriptions, paid invoices, one-time Checkout purchases and refunds, everything Stripe holds in that environment. Repeating a sync is idempotent. Poll its progress before assuming history is complete.

For existing Stripe customers without metadata, map the Stripe customer ID to your authenticated app user through Web and Stripe → Customer identity, or with the MCP link_marketplace_user tool over an owner or admin OAuth sign-in or a write-enabled MCP token. A server key cannot link customers, and neither can a setup token. Confirm account ownership before linking. Pending subscription state is applied after identity resolution; unidentified purchases are not assigned to a guessed user. See Link a Stripe customer to your user.

Keep using your server's Stripe Billing Portal integration to manage payment methods, cancellations and invoices. CashSDK observes the resulting events. If using CashSDK's portal API instead, follow its permission and return-origin requirements.

Set it up through MCP#

Connect the remote MCP server using OAuth or a scoped MCP token. Ask your agent:

text
Create an app with platforms ["stripe"] and no bundle ID.
Read get_stripe_integration_snippets and get_setup_checklist.
Connect my Stripe account, import the selected prices with their entitlement IDs,
and sync existing subscriptions. Verify access for my test customer.
Keep my current Checkout and do not add a client SDK.

The tools are list_supported_platforms, create_app, get_stripe_integration_snippets, get_marketplace_status, list_stripe_prices, import_stripe_prices, sync_stripe and get_stripe_sync. Setup tokens can inspect setup but cannot install Stripe credentials, import Stripe prices, start a sync or link customers. Use owner/admin OAuth or a write-enabled MCP token for those changes, or complete them in the dashboard. Keep secrets out of prompts and logs.

Verify before launch#

Check a paid purchase, renewal, cancellation at period end, full refund, duplicate webhook delivery and two different customers. Verify the entitlement from your backend after each event, including the removal of access after a full refund.

The Stripe-only checklist checks imported prices, access mapping, a received sandbox payment and attribution to an app user. It does not require an SDK ping, hosted paywall, offering or browser origin. These checks cover Sandbox; verify Production with a live purchase.

Go live#

Add a Production connection next to the Sandbox one, with a live restricted key and its own webhook URL registered in Stripe's live mode. Import the live prices, which have new IDs, with the same entitlements. Then read access with a live CashSDK key. Sandbox keeps working for tests. Go live with Stripe has every step, including a first live purchase and refund.

Add web, direct macOS, Chrome, iOS or Android later with the same app user IDs and shared entitlements.