Gumroad supports PayPal through two integrations: PayPal Commerce Platform (PCP) for modern accounts and legacy Braintree for existing integrations.
PayPal Commerce Platform provides:
- Direct PayPal checkout for customers
- PayPal business account payouts for sellers
- OAuth-based partner integration
- Webhook event notifications
Supported Countries
PayPal Commerce Platform is available in most countries except:
COUNTRY_CODES_NOT_SUPPORTED_BY_PCP = [
"DZ", # Algeria
"BR", # Brazil
"EG", # Egypt
"IN", # India
"IL", # Israel
"JP", # Japan
"FM", # Micronesia
"MA", # Morocco
"TR" # Turkey
]
Sellers in unsupported countries may still use PayPal for receiving customer payments, but cannot use PayPal Commerce Platform for payouts.
Configuration
Environment Variables
Configure PayPal in your .env file:
# Standard PayPal Credentials
PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_client_secret
PAYPAL_WEBHOOK_ID=your_webhook_id
PAYPAL_USERNAME=your_api_username
PAYPAL_PASSWORD=your_api_password
PAYPAL_SIGNATURE=your_api_signature
PAYPAL_MERCHANT_EMAIL=[email protected]
PAYPAL_BN_CODE=your_bn_code
# PayPal Partner Credentials (for seller onboarding)
PAYPAL_PARTNER_CLIENT_ID=partner_client_id
PAYPAL_PARTNER_CLIENT_SECRET=partner_client_secret
PAYPAL_PARTNER_MERCHANT_EMAIL=[email protected]
PAYPAL_PARTNER_MERCHANT_ID=partner_merchant_id
Gemfile Dependencies
gem "paypal-sdk-merchant", "~> 1.117"
gem "paypal-checkout-sdk", "~> 1.0"
Connecting PayPal Accounts
Sellers connect PayPal through the payment settings page.
Connection Flow
Initiate Connection
Seller clicks “Connect PayPal” in Settings > Payments# app/controllers/paypal_controller.rb
def connect
paypal_merchant_account_manager = PaypalMerchantAccountManager.new
response = paypal_merchant_account_manager.create_partner_referral(
current_seller,
paypal_connect_settings_payments_url
)
redirect_to response[:redirect_url]
end
OAuth Authorization
Seller is redirected to PayPal to:
- Log into their PayPal business account
- Grant permissions to Gumroad
- Confirm email address
Return to Gumroad
PayPal redirects back with merchant ID:def paypal_connect
PaypalMerchantAccountManager.new.update_merchant_account(
user: current_seller,
paypal_merchant_id: params[:merchantIdInPayPal],
meta: params.slice(:merchantId, :permissionsGranted)
)
end
Verification
Gumroad verifies the account meets requirements:
- Primary email is confirmed
- Payments receivable permission granted
- OAuth integration properly configured
Eligibility Requirements
To connect PayPal, sellers must:
-
Minimum Sales: $100 in total sales on Gumroad
MIN_SALES_CENTS_REQ_FOR_PCP = 100_00 # $100
-
Country Support: Be in a PCP-supported country
-
Email Confirmation: Have confirmed PayPal email
-
Permissions: Grant required OAuth permissions
If the minimum sales requirement is not met, sellers receive an error message and cannot connect PayPal Commerce Platform.
PayPal Webhooks
Gumroad listens for PayPal webhook events to track account status.
Webhook Configuration
Register webhooks using the PayPal API:
curl -X POST https://api.paypal.com/v1/notifications/webhooks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{
"url": "https://gumroad.com/paypal-webhook",
"event_types": [
{"name": "CHECKOUT.ORDER.PROCESSED"},
{"name": "CUSTOMER.DISPUTE.CREATED"},
{"name": "CUSTOMER.DISPUTE.RESOLVED"},
{"name": "CUSTOMER.DISPUTE.UPDATED"},
{"name": "MERCHANT.PARTNER-CONSENT.REVOKED"},
{"name": "PAYMENT.CAPTURE.COMPLETED"},
{"name": "PAYMENT.CAPTURE.DENIED"},
{"name": "PAYMENT.CAPTURE.PENDING"},
{"name": "PAYMENT.CAPTURE.REFUNDED"},
{"name": "PAYMENT.CAPTURE.REVERSED"},
{"name": "PAYMENT.ORDER.CREATED"},
{"name": "PAYMENT.REFERENCED-PAYOUT-ITEM.COMPLETED"}
]
}'
Handled Events
Account Updated
Consent Revoked
Updates merchant account when status changes:# Events:
# - MERCHANT.ONBOARDING.SELLER-GRANTED-CONSENT
# - MERCHANT.ONBOARDING.COMPLETED
# - MERCHANT.PARTNER-EMAIL-CONFIRMED
# - MERCHANT.CAPABILITY.UPDATED
# - MERCHANT.PARTNER-SUBSCRIPTION-UPDATED
def handle_merchant_account_updated_event(paypal_event)
tracking_id = paypal_event["resource"]["tracking_id"]
user = User.find_by_external_id(tracking_id.split("-")[0])
update_merchant_account(
user: user,
paypal_merchant_id: paypal_event["resource"]["merchant_id"],
create_new: false
)
end
Handles when seller revokes Gumroad’s access:# Events:
# - MERCHANT.PARTNER-CONSENT.REVOKED
# - IDENTITY.AUTHORIZATION-CONSENT.REVOKED
def handle_merchant_consent_revoked_event(paypal_event)
merchant_id = paypal_event["resource"]["merchant_id"]
merchant_accounts = MerchantAccount.where(
charge_processor_id: PaypalChargeProcessor.charge_processor_id,
charge_processor_merchant_id: merchant_id
)
merchant_accounts.each do |account|
account.delete_charge_processor_account!
MerchantRegistrationMailer.account_deauthorized_to_user(
account.user_id,
PaypalChargeProcessor.charge_processor_id
).deliver_later
end
end
PayPal for Customer Payments
Customers can pay with PayPal during checkout.
Order Creation
# app/controllers/paypal_controller.rb
def order
product = Link.find_by_external_id(params[:product][:external_id])
order_id = PaypalChargeProcessor.create_order_from_product_info(
params[:product]
)
render json: { order_id: order_id }
end
Billing Agreements
For subscriptions, PayPal uses billing agreements:
def billing_agreement_token
billing_agreement_token_id = PaypalChargeProcessor
.generate_billing_agreement_token(
shipping: params[:shipping] == "true"
)
render json: { billing_agreement_token_id: }
end
def billing_agreement
response = PaypalChargeProcessor.create_billing_agreement(
billing_agreement_token_id: params[:billing_agreement_token_id]
)
render json: response
end
Billing agreements allow Gumroad to charge the customer’s PayPal account for recurring subscriptions without requiring re-authorization.
PayPal Instant Payment Notification (IPN)
Gumroad also supports PayPal’s legacy IPN system for backward compatibility.
IPN Endpoints
Disconnecting PayPal
Sellers can disconnect PayPal from Settings > Payments:
def disconnect
render json: {
success: PaypalMerchantAccountManager.new.disconnect(
user: current_seller
)
}
end
Sellers cannot disconnect PayPal if they have active subscriptions or preorders being charged through PayPal.
PayPal Payouts
When PayPal is the payout method:
def current_payout_processor
if (paypal_payout_email.present? && active_bank_account.blank?) ||
!native_payouts_supported?
PayoutProcessorType::PAYPAL
else
PayoutProcessorType::STRIPE
end
end
Payouts are sent to the seller’s payment_address (PayPal email) when:
- No bank account is connected, OR
- Seller’s country doesn’t support native Stripe payouts
Testing PayPal Integration
For development:
-
Sandbox Mode: Use PayPal sandbox credentials
PAYPAL_ENV = "sandbox"
PAYPAL_REST_ENDPOINT = "https://api.sandbox.paypal.com"
-
Test Accounts: Create test buyer and seller accounts in PayPal Developer Dashboard
-
Webhook Testing: Use webhook.site to inspect webhook payloads
Troubleshooting
Email Not Confirmed
Issue: PayPal account email is not confirmed
Solution: Seller receives email to confirm their PayPal email address
MerchantRegistrationMailer.confirm_email_on_paypal(
user.id,
paypal_email
).deliver_later
Missing Permissions
Issue: Seller didn’t grant all required permissions during OAuth
Solution: Account marked as incomplete, seller must reconnect and grant permissions
Country Not Supported
Issue: Seller’s PayPal account is in an unsupported country
Solution: Merchant account is deleted, error message displayed
if PaypalMerchantAccountManager::COUNTRY_CODES_NOT_SUPPORTED_BY_PCP
.include?(paypal_account_country_code)
merchant_account.delete_charge_processor_account!
return "Your PayPal account could not be connected because this PayPal integration is not supported in your country."
end