Whop × Agree.com

Integration Guide

Add Whop as a payment provider alongside Stripe — no rip-and-replace. Both run side-by-side in production until you’re ready to cut over.

Stripe PaymentIntent → Whop Checkout Stripe Connect → Whop KYC/KYB stripity_stripe 3.2.0 → Whop REST API Phoenix + Ecto stays React 19 + Vite stays No new dependencies

Start Here — Drop This Into Your Repo

This file gives Cursor and Claude Code full context about your Whop migration. Every file path, schema change, and code example — ready to implement.

Save as CLAUDE.md in your project root. Open Cursor. Say “implement the Whop migration.”

Implement a production Stripe → Whop migration for @agree/web (Phoenix 1.7 + React 19). Whop runs side-by-side with Stripe. No rip-and-replace. Flow 1 — SaaS billing: Replace Stripe.PaymentIntent / Customer / SetupIntent in lib/agree/payments.ex Flow 2 — Merchant onboarding: Replace Stripe Connect (submit_kyc, finish_kyc) + post-signature checkout Key files: lib/agree/payments.ex, lib/agree/accounts/organization.ex, lib/agree_web/controllers/webhooks/stripe/, assets/js/views/payments/, assets/js/views/settings/Banking.tsx, lib/agree/workers/stripe.ex Create: Agree.Payments.Whop.Client, AgreeWeb.Webhooks.WhopController, Agree.Workers.Whop, Ecto migration for whop_* columns + idempotency table. Rollout: PAYMENT_PROVIDER env var defaults to "stripe". Per-org override via Organization.external_metadata["payment_provider"]. No new deps needed. Internal orgs first, then small cohort, then full rollout. Webhook handlers must update both merchant and customer dashboard state. Accounting sync (QuickBooks/Xero) must produce identical entries to Stripe path.

Rollout Strategy

Whop runs alongside Stripe in production. No staging environment needed, no new dependencies. Routing is controlled by an env var with per-org override using your existing Organization.external_metadata field.

PAYMENT_PROVIDER env var defaults to "stripe". Per-org override: Organization.external_metadata["payment_provider"] == "whop". All routing lives in Agree.Payments — one check, not scattered across 52 files.
1

Build

Whop client, webhook controller, Ecto migration, both flow paths. Stripe stays default. Deploy with zero behavior change.

2

Internal Test

Set external_metadata["payment_provider"] to "whop" on your own Agree org. Test with real money internally.

3

Small Cohort

Enable Whop for 5–10 trusted merchants. Verify webhook parity, accounting sync, and payout timing.

4

Full Rollout

Flip PAYMENT_PROVIDER=whop globally. Stripe path stays in code as fallback until you’re confident.

Two Flows

Your integration has two independent tracks. Each routes through Agree.Payments based on the provider check.

Flow 1: SaaS Billing

CustomerSaaS User
PaymentWhop Checkout
SubscriptionWhop Membership
AccessSoftware

Replaces Stripe.PaymentIntent, Stripe.Customer, Stripe.SetupIntent in lib/agree/payments.ex

Flow 2: Contract → Checkout → Payout

ContractSigned
MerchantWhop KYC/KYB
PaymentWhop Checkout
PayoutWhop Payouts

Replaces Stripe.Account.create, Payments.submit_kyc, create_or_update_stripe_connected_account

Your Code → Whop

How your existing Ecto schemas and modules map to Whop. These are the columns that get added.

Your SchemaFileWhop EntityNew Columns
Organization lib/agree/accounts/organization.ex Company (submerchant) whop_company_id, whop_verification_status
Invoice lib/agree/payments/invoice.ex Checkout Session + Payment whop_checkout_session_id, whop_payment_id
InvoiceTemplate lib/agree/payments/invoice_template.ex Plan whop_plan_id
Transaction lib/agree/payments/transaction.ex Payment / Payout whop_payment_id, whop_payout_id
PaymentMethod lib/agree/payments/payment_method.ex Checkout method selection Methods become checkout-config driven

What Changes

Side-by-side: the Stripe calls you have today vs. the Whop equivalents.

Flow 1: SaaS Billing

Stripe (Today)

  • Stripe.Customer.create
  • Stripe.PaymentIntent.create
  • Stripe.SetupIntent for saved methods
  • Webhook: payment_intent.succeeded

Whop (After)

  • Whop plan from InvoiceTemplate
  • Whop checkout session
  • Methods handled inline by checkout
  • Webhook: payment.succeeded + membership.went_valid

Flow 2: Merchant Onboarding

Stripe Connect (Today)

  • Stripe.Account.create
  • Stripe.AccountLink.create for KYC
  • submit_kyc / finish_kyc
  • Webhook: account.updated

Whop (After)

  • Connected company per Organization
  • Whop KYC/KYB flow
  • Status tracked via company.updated
  • All onboarding managed by Whop

Webhook Mapping

Your existing 13 Stripe event handlers map to these Whop events. Both webhook endpoints run simultaneously.

Stripe EventWhop EventWhat Happens
payment_intent.succeededpayment.succeededInvoice marked paid, QBO/Xero sync
payment_intent.payment_failedpayment.failedFailed status, retry alert
charge.refundedpayment.refundedRefund transaction, fee update
account.updatedcompany.updatedMerchant KYC status refreshed
payout.*payout.*Payout timeline in merchant dashboard
setup_intent.succeededpayment_method.attachedPayment method readiness updated
Outbound webhooks stay identical. Your Agree.Workers.Webhook still fires invoice.paid, invoice.failed, etc. to customer endpoints with the same payload shapes.

Architecture

Where Whop fits. Orange is new, green stays unchanged. Both providers coexist.

        CUSTOMERS                         CONTRACT SIGNERS
             |                                  |
      Agree Dashboard                   Agree Sign Flow
      (React 19 + Vite)                  (AcceptView.jsx)
             |                                  |
             v                                  v
      +-------------------------------------------+
      |          Agree Backend (Phoenix)          |
      |    lib/agree/payments.ex                  |
      |    provider = external_metadata || env    |
      +-------------------------------------------+
          |          |            |            |
     Ecto/PG   Stripe API   Whop REST API   Oban
     (stays)   (default)    (per-org)       (stays)

Merchant Onboarding States

Flow 2 merchants progress through these states. Stored as organization.whop_verification_status.

StateMeaningCan DoBlocked
invitedNot startedView checklistCheckout, payouts
kyc_pendingKYC in progressContinue verificationCheckout, payouts
restrictedMissing docsUpload documentsCheckout, payouts
activeFully verifiedCheckout, payments, payoutsNone
suspendedCompliance holdRead-onlyAll money movement

Timeline

One week to first live dollar. Stripe stays live the entire time.

Day 1–2
Build
  • Whop.Client + webhook controller
  • Ecto migration
  • Both flow paths in Payments
  • Deploy — Stripe still default
Day 3–4
Internal Test
  • Enable Whop on your own org
  • Real money, real webhooks
  • Verify QBO/Xero sync parity
Day 5–6
Small Cohort
  • 5–10 trusted merchants
  • Webhook parity confirmed
  • Payout timing validated
Day 7
Full Rollout
  • Flip env var to “whop”
  • Stripe path stays as fallback
  • First live dollar

What Stays the Same

Most of your stack is unchanged. Whop replaces only the payment provider layer.

Phoenix 1.7 + LiveView
Ecto + PostgreSQL
Oban job processing
React 19 + Vite
Tailwind + Radix + AUI
Docker + Fly.io
Auth system
Contract signing flow
Usage metering
Outbound webhook shapes
No new dependencies
~ QuickBooks / Xero
~ Banking.tsx
Bottom line: Auth, contracts, metering, and all integrations are untouched. Stripe keeps running as the default until you decide to flip the switch. No new dependencies, no new infra.