Documentation

Stripe

Connect Stripe for payments, subscriptions, payouts, and webhook event processing.

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:

POST/storefront/stripe/intentNo auth

Body 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)

POST/storefront/stripe/checkout-sessionNo auth

Returns a Stripe-hosted checkout URL. Use this when you want Stripe to handle the entire payment UI rather than embedding Elements.

Subscriptions

POST/storefront/stripe/subscription-sessionNo auth

Same as checkout-session but creates a subscription instead of a one-shot payment. Used by Subscriptions and rentals.

Webhooks

POST/connect/webhook/stripeNo auth

Stripe 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 paid
  • payment_intent.payment_failed → mark order failed, send dunning
  • invoice.payment_succeeded → renew subscription, create renewal order
  • invoice.payment_failed → mark subscription past-due
  • charge.refunded → reverse the order
  • customer.subscription.deleted → cancel subscription record
  • account.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

GET/storefront/verify-payment/:provider/:configId/:paymentIdNo auth

Used 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:

POST/storefront/order/refund/:orderNumberJWT

The 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 call stripe.confirmCardPayment (not the platform).
  • Idempotency — Stripe SDK calls go through the Upstream service, which sets the Idempotency-Key header 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.