Skip to main content
Gumroad supports PayPal through two integrations: PayPal Commerce Platform (PCP) for modern accounts and legacy Braintree for existing integrations.

PayPal Commerce Platform

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

1

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
2

OAuth Authorization

Seller is redirected to PayPal to:
  • Log into their PayPal business account
  • Grant permissions to Gumroad
  • Confirm email address
3

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
4

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:
  1. Minimum Sales: $100 in total sales on Gumroad
    MIN_SALES_CENTS_REQ_FOR_PCP = 100_00  # $100
    
  2. Country Support: Be in a PCP-supported country
  3. Email Confirmation: Have confirmed PayPal email
  4. 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

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

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:
  1. Sandbox Mode: Use PayPal sandbox credentials
    PAYPAL_ENV = "sandbox"
    PAYPAL_REST_ENDPOINT = "https://api.sandbox.paypal.com"
    
  2. Test Accounts: Create test buyer and seller accounts in PayPal Developer Dashboard
  3. 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

Build docs developers (and LLMs) love