Skip to content

Payment Gateways

All payment operations go through a standard adapter interface. Each gateway implements the same contract, and the rest of saas-core interacts only with the interface.

Commerce API
├── Adapter Interface
│ ├── createCheckout()
│ ├── verifyWebhook()
│ ├── cancelSubscription()
│ └── getSubscription()
├── LemonSqueezyAdapter (Phase 3)
└── AppleIAPAdapter (Phase 4)
1. User clicks "Upgrade" in Acme
→ Acme backend calls saas-core: POST /commerce/checkout
→ saas-core creates a checkout URL with custom_data
→ Returns checkout URL to Acme
2. User completes payment on LemonSqueezy
3. LS sends webhook to saas-core: POST /api/webhooks/lemonsqueezy
→ saas-core verifies webhook signature
→ Updates subscription status
→ Creates outbox event
4. saas-core outbox worker sends event to Acme webhook

Checkout requests include custom_data that saas-core uses to correlate the webhook back to the correct record:

{
"app_id": "tobby",
"user_id": "u_xxx",
"sku": "tobby.pro.monthly",
"subscription_id": "sub_xxx"
}
1. User purchases in iOS StoreKit
→ iOS App gets transaction_id
2. iOS App sends transaction_id to its backend
→ Backend calls saas-core: POST /commerce/iap/verify
3. saas-core verifies with Apple App Store Server API
→ Upserts subscription record
→ Returns standardized Subscription object
4. Apple sends renewal/refund notifications
→ saas-core updates subscription status

Apple IAP products are mapped to saas-core prices via the gateway_product_id field in the price table. When an IAP verification comes in, saas-core looks up the gateway_product_id to find the corresponding SKU and application.

All gateway webhooks follow the same processing pipeline:

  1. Signature verification — reject forged webhooks
  2. Idempotency check — deduplicate by gateway event ID
  3. State transition — update subscription with optimistic locking
  4. Outbox event — create transaction outbox record for delivery