Stripe is the default payment processor in AppEngine. The integration covers four distinct surfaces: payment intents (one-time charges), checkout sessions (hosted checkout), subscriptions (recurring billing), and connected accounts (multi-merchant payouts). All of them route through the same Stripe credential stored against the org.
Connect Stripe
Two paths:
- API key mode — paste your Stripe secret key and webhook secret. Fastest for single-account setups.
- OAuth mode (Stripe Connect) — used when the platform is the marketplace and individual users have their own Stripe accounts (creators, sellers, vendors). Each user goes through Stripe's OAuth consent and gets their own connected account ID stored against their customer record.
API-key setup:
await fetch('/api/upstream/save-integration', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
type: 'stripe-provider',
name: 'Stripe Production',
credentials: {
secretKey: 'sk_live_...',
publishableKey: 'pk_live_...',
webhookSecret: 'whsec_...',
},
publicConfig: { currency: 'USD', countryCode: 'US' },
}),
});
OAuth setup follows the universal flow — see OAuth flow. The vendor segment is stripe.
Payment intent (one-shot)
The storefront's primary checkout call:
/storefront/stripe/intentNo authBody specifies the cart, customer, and shipping. Returns a clientSecret the browser uses with stripe.confirmCardPayment().
const { clientSecret } = await fetch('/api/storefront/stripe/intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json', orgid: ORG_ID },
body: JSON.stringify({
cart: cartObject,
customer: { email: '[email protected]' },
shippingAddress,
}),
}).then(r => r.json());
const stripe = window.Stripe('pk_live_...');
await stripe.confirmCardPayment(clientSecret, {
payment_method: { card: cardElement },
});
The intent is created server-side with automatic_payment_methods enabled, so wallets (Apple Pay, Google Pay, Link) appear automatically alongside cards.
Checkout session (hosted)
/storefront/stripe/checkout-sessionNo authReturns a Stripe-hosted checkout URL. Use this when you want Stripe to handle the entire payment UI rather than embedding Elements.
Subscriptions
/storefront/stripe/subscription-sessionNo authSame as checkout-session but creates a subscription instead of a one-shot payment. Used by Subscriptions and rentals.
Webhooks
/connect/webhook/stripeNo authStripe POSTs events here. The Connect module verifies the signature against the webhook secret stored on the org's integration, then dispatches the event:
payment_intent.succeeded→ mark order paidpayment_intent.payment_failed→ mark order failed, send dunninginvoice.payment_succeeded→ renew subscription, create renewal orderinvoice.payment_failed→ mark subscription past-duecharge.refunded→ reverse the ordercustomer.subscription.deleted→ cancel subscription recordaccount.updated(Connect) → update merchant account state
The full handler is at src/connect/vendor/stripe.connect.ts. Application code subscribes to events via the platform's event emitter (stripe:payment_intent.succeeded).
Connected accounts (Stripe Connect)
For marketplaces — sellers each have their own Stripe account. The integration stores both the platform's secret key and a per-seller connectedAccountId.
A payment to a connected account uses transfer_data in the intent:
{
"amount": 10000,
"currency": "usd",
"transfer_data": {
"destination": "acct_seller_xyz"
},
"application_fee_amount": 500
}
The platform keeps application_fee_amount (here, $5.00 of the $100.00). The seller receives the rest. Payouts to the seller's bank happen on Stripe's schedule — the platform doesn't run a payout cycle for Connect accounts.
Onboard a new seller via Stripe's hosted onboarding (AccountLink):
const { url } = await fetch('/api/upstream/call/stripe-provider/create-account-link', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
customerId: 'cust-seller-1',
refreshUrl: 'https://shop.example/onboard/refresh',
returnUrl: 'https://shop.example/onboard/complete',
}),
}).then(r => r.json());
Verifying a payment
/storefront/verify-payment/:provider/:configId/:paymentIdNo authUsed as a fallback when webhook delivery is delayed. The order success page calls verify-payment/stripe/<configId>/<intentId>; the platform queries Stripe directly for the latest status and updates the order if needed.
Refunds
Refunds for orders run through the storefront refund endpoint, which routes to Stripe internally:
/storefront/order/refund/:orderNumberJWTThe body specifies amount and items. The platform calls stripe.refunds.create against the original payment intent.
Common quirks
- Webhook signature failures — almost always wrong webhook secret. Re-copy from the Stripe dashboard, save with
save-integration. - 3DS / SCA failures — the intent comes back with
requires_action. The browser must callstripe.confirmCardPayment(not the platform). - Idempotency — Stripe SDK calls go through the Upstream service, which sets the
Idempotency-Keyheader per request. Don't add a second one.
For the Stripe test mode flow, use a separate integration record (e.g. "Stripe Test") with test keys. Don't toggle a single record between live and test.