PayPal is the only payment method using a completely separate processor. The "Braintree Bridge" pattern creates unique handling across the entire lifecycle. Every other payment method flows natively through Stripe. PayPal is the existence proof that internal models mirror Stripe's models — shimmed into a Stripe-shaped world, it takes the wrong path at every seam.

AspectStripe-Native PathPayPal / Braintree Path
Token storageStore PM in Stripe as pm_XXXXXXCreate Braintree customer, vault PayPal nonce → Billing Agreement
Stripe metadataNative PM attached to customerStore braintree_id + external_payment_methods in customer metadata
Collection methodcharge_automaticallySwitch ALL subs to send_invoice + days_until_due
Supported methodsCards, Apple Pay, Google Pay, ACH, CashApp, Link, SEPA DebitPayPal only

Critical gotcha: adding PayPal changes the collection method on every subscription the customer has, even if they also have a credit card on file. Removing PayPal reverses this — collection_method reverts to charge_automatically.

AspectStripe (Card)PayPal (Braintree)
On-session routinggateOnSessionFlow() returns true — direct chargegateOnSessionFlow() returns false — always off-session path
Payment callInvoicesPay — direct charge, funds land in StripeBraintree ChargePayPalAccount (GraphQL) → InvoicesPay with PaidOutOfBand
Failure handlingVoid invoice, rollback subscription changesSkip rollback, leave invoice unpaid for dunning
Dedup mechanismStripe idempotency keysbraintree_transaction_id check before charge
AspectStripe (Card)PayPal (Braintree)
Collection modecharge_automatically — Stripe handles payment entirelysend_invoice — Stripe creates invoice but does NOT collect
subs-api roleReceives webhook, no collection action neededReceives webhook, initiates Braintree charge, marks PaidOutOfBand
PipelineStripe Event → Cloud Functions → Pub/Sub → billing-webhooks → doneSame pipeline → subs-api → Braintree charge → mark paid
AspectStripe (Card)PayPal (Braintree)
Trigger eventinvoice.payment_failedinvoice.overdue
Retry mechanismStripe Smart Retries (automatic)Custom retry via subs-api + Braintree
Handler codeStripe internal retry logicendpoints_stripe_dunning.go
Terminal stateinvoice.marked_uncollectible → cancel subscriptions → ban accountinvoice.marked_uncollectible → cancel subscriptions → ban account

Refund routing is determined by checking braintree_transaction_id in the invoice metadata. If present, the payment went through Braintree and must be refunded there — Stripe only gets a credit note. If absent, standard Stripe Refund API. Two systems updated for PayPal refunds, one transaction.

MethodGatewayStripe PM TypeCollection ModeOn-Session
Credit CardStripecardcharge_automaticallyYes
Apple PayStripecard (wallet)charge_automaticallyYes
Google PayStripecard (wallet)charge_automaticallyYes
ACH Direct DebitStripeus_bank_accountcharge_automaticallyYes
CashAppStripecashappcharge_automaticallyYes
LinkStripelinkcharge_automaticallyYes
SEPA DebitStripesepa_debitcharge_automaticallyYes
PayPalBraintreeN/Asend_invoiceNo