6# PayPal Integration
7
8Master PayPal payment integration including Express Checkout, IPN handling, recurring billing, and refund workflows.
9
10## Do not use this skill when
11
12- The task is unrelated to paypal integration
13- You need a different domain or tool outside this scope
14
15## Instructions
16
17- Clarify goals, constraints, and required inputs.
18- Apply relevant best practices and validate outcomes.
19- Provide actionable steps and verification.
20
21## Use this skill when
22
23- Integrating PayPal as a payment option
24- Implementing express checkout flows
25- Setting up recurring billing with PayPal
26- Processing refunds and payment disputes
27- Handling PayPal webhooks (IPN)
28- Supporting international payments
29- Implementing PayPal subscriptions
30
31## Core Concepts
32
33### 1. Payment Products
34**PayPal Checkout**
35- One-time payments
36- Express checkout experience
37- Guest and PayPal account payments
38
39**PayPal Subscriptions**
40- Recurring billing
41- Subscription plans
42- Automatic renewals
43
44**PayPal Payouts**
45- Send money to multiple recipients
46- Marketplace and platform payments
47
48### 2. Integration Methods
49**Client-Side (JavaScript SDK)**
50- Smart Payment Buttons
51- Hosted payment flow
52- Minimal backend code
53
54**Server-Side (REST API)**
55- Full control over payment flow
56- Custom checkout UI
57- Advanced features
58
59### 3. IPN (Instant Payment Notification)
60- Webhook-like payment notifications
61- Asynchronous payment updates
62- Verification required
63
64## Quick Start
65
66```javascript
67// Frontend - PayPal Smart Buttons
68<div id="paypal-button-container"></div>
69
70<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD"></script>
71<script>
72 paypal.Buttons({
73 createOrder: function(data, actions) {
74 return actions.order.create({
75 purchase_units: [{
76 amount: {
77 value: '25.00'
78 }
79 }]
80 });
81 },
82 onApprove: function(data, actions) {
83 return actions.order.capture().then(function(details) {
84 // Payment successful
85 console.log('Transaction completed by ' + details.payer.name.given_name);
86
87 // Send to backend for verification
88 fetch('/api/paypal/capture', {
89 method: 'POST',
90 headers: {'Content-Type': 'application/json'},
91 body: JSON.stringify({orderID: data.orderID})
92 });
93 });
94 }
95 }).render('#paypal-button-container');
96</script>
97```
98
99```python
100# Backend - Verify and capture order
101from paypalrestsdk import Payment
102import paypalrestsdk
103
104paypalrestsdk.configure({
105 "mode": "sandbox", # or "live"
106 "client_id": "YOUR_CLIENT_ID",
107 "client_secret": "YOUR_CLIENT_SECRET"
108})
109
110def capture_paypal_order(order_id):
111 """Capture a PayPal order."""
112 payment = Payment.find(order_id)
113
114 if payment.execute({"payer_id": payment.payer.payer_info.payer_id}):
115 # Payment successful
116 return {
117 'status': 'success',
118 'transaction_id': payment.id,
119 'amount': payment.transactions[0].amount.total
120 }
121 else:
122 # Payment failed
123 return {
124 'status': 'failed',
125 'error': payment.error
126 }
127```
128
129## Express Checkout Implementation
130
131### Server-Side Order Creation
132```python
133import requests
134import json
135
136class PayPalClient:
137 def __init__(self, client_id, client_secret, mode='sandbox'):
138 self.client_id = client_id
139 self.client_secret = client_secret
140 self.base_url = 'https://api-m.sandbox.paypal.com' if mode == 'sandbox' else 'https://api-m.paypal.com'
141 self.access_token = self.get_access_token()
142
143 def get_access_token(self):
144 """Get OAuth access token."""
145 url = f"{self.base_url}/v1/oauth2/token"
146 headers = {"Accept": "application/json", "Accept-Language": "en_US"}
147
148 response = requests.post(
149 url,
150 headers=headers,
151 data={"grant_type": "client_credentials"},
152 auth=(self.client_id, self.client_secret)
153 )
154
155 return response.json()['access_token']
156
157 def create_order(self, amount, currency='USD'):
158 """Create a PayPal order."""
159 url = f"{self.base_url}/v2/checkout/orders"
160 headers = {
161 "Content-Type": "application/json",
162 "Authorization": f"Bearer {self.access_token}"
163 }
164
165 payload = {
166 "intent": "CAPTURE",
167 "purchase_units": [{
168 "amount": {
169 "currency_code": currency,
170 "value": str(amount)
171 }
172 }]
173 }
174
175 response = requests.post(url, headers=headers, json=payload)
176 return response.json()
177
178 def capture_order(self, order_id):
179 """Capture payment for an order."""
180 url = f"{self.base_url}/v2/checkout/orders/{order_id}/capture"
181 headers = {
182 "Content-Type": "application/json",
183 "Authorization": f"Bearer {self.access_token}"
184 }
185
186 response = requests.post(url, headers=headers)
187 return response.json()
188
189 def get_order_details(self, order_id):
190 """Get order details."""
191 url = f"{self.base_url}/v2/checkout/orders/{order_id}"
192 headers = {
193 "Authorization": f"Bearer {self.access_token}"
194 }
195
196 response = requests.get(url, headers=headers)
197 return response.json()
198```
199
200## IPN (Instant Payment Notification) Handling
201
202### IPN Verification and Processing
203```python
204from flask import Flask, request
205import requests
206from urllib.parse import parse_qs
207
208app = Flask(__name__)
209
210@app.route('/ipn', methods=['POST'])
211def handle_ipn():
212 """Handle PayPal IPN notifications."""
213 # Get IPN message
214 ipn_data = request.form.to_dict()
215
216 # Verify IPN with PayPal
217 if not verify_ipn(ipn_data):
218 return 'IPN verification failed', 400
219
220 # Process IPN based on transaction type
221 payment_status = ipn_data.get('payment_status')
222 txn_type = ipn_data.get('txn_type')
223
224 if payment_status == 'Completed':
225 handle_payment_completed(ipn_data)
226 elif payment_status == 'Refunded':
227 handle_refund(ipn_data)
228 elif payment_status == 'Reversed':
229 handle_chargeback(ipn_data)
230
231 return 'IPN processed', 200
232
233def verify_ipn(ipn_data):
234 """Verify IPN message authenticity."""
235 # Add 'cmd' parameter
236 verify_data = ipn_data.copy()
237 verify_data['cmd'] = '_notify-validate'
238
239 # Send back to PayPal for verification
240 paypal_url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' # or production URL
241
242 response = requests.post(paypal_url, data=verify_data)
243
244 return response.text == 'VERIFIED'
245
246def handle_payment_completed(ipn_data):
247 """Process completed payment."""
248 txn_id = ipn_data.get('txn_id')
249 payer_email = ipn_data.get('payer_email')
250 mc_gross = ipn_data.get('mc_gross')
251 item_name = ipn_data.get('item_name')
252
253 # Check if already processed (prevent duplicates)
254 if is_transaction_processed(txn_id):
255 return
256
257 # Update database
258 # Send confirmation email
259 # Fulfill order
260 print(f"Payment completed: {txn_id}, Amount: ${mc_gross}")
261
262def handle_refund(ipn_data):
263 """Handle refund."""
264 parent_txn_id = ipn_data.get('parent_txn_id')
265 mc_gross = ipn_data.get('mc_gross')
266
267 # Process refund in your system
268 print(f"Refund processed: {parent_txn_id}, Amount: ${mc_gross}")
269
270def handle_chargeback(ipn_data):
271 """Handle payment reversal/chargeback."""
272 txn_id = ipn_data.get('txn_id')
273 reason_code = ipn_data.get('reason_code')
274
275 # Handle chargeback
276 print(f"Chargeback: {txn_id}, Reason: {reason_code}")
277```
278
279## Subscription/Recurring Billing
280
281### Create Subscription Plan
282```python
283def create_subscription_plan(name, amount, interval='MONTH'):
284 """Create a subscription plan."""
285 client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
286
287 url = f"{client.base_url}/v1/billing/plans"
288 headers = {
289 "Content-Type": "application/json",
290 "Authorization": f"Bearer {client.access_token}"
291 }
292
293 payload = {
294 "product_id": "PRODUCT_ID", # Create product first
295 "name": name,
296 "billing_cycles": [{
297 "frequency": {
298 "interval_unit": interval,
299 "interval_count": 1
300 },
301 "tenure_type": "REGULAR",
302 "sequence": 1,
303 "total_cycles": 0, # Infinite
304 "pricing_scheme": {
305 "fixed_price": {
306 "value": str(amount),
307 "currency_code": "USD"
308 }
309 }
310 }],
311 "payment_preferences": {
312 "auto_bill_outstanding": True,
313 "setup_fee": {
314 "value": "0",
315 "currency_code": "USD"
316 },
317 "setup_fee_failure_action": "CONTINUE",
318 "payment_failure_threshold": 3
319 }
320 }
321
322 response = requests.post(url, headers=headers, json=payload)
323 return response.json()
324
325def create_subscription(plan_id, subscriber_email):
326 """Create a subscription for a customer."""
327 client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
328
329 url = f"{client.base_url}/v1/billing/subscriptions"
330 headers = {
331 "Content-Type": "application/json",
332 "Authorization": f"Bearer {client.access_token}"
333 }
334
335 payload = {
336 "plan_id": plan_id,
337 "subscriber": {
338 "email_address": subscriber_email
339 },
340 "application_context": {
341 "return_url": "https://yourdomain.com/subscription/success",
342 "cancel_url": "https://yourdomain.com/subscription/cancel"
343 }
344 }
345
346 response = requests.post(url, headers=headers, json=payload)
347 subscription = response.json()
348
349 # Get approval URL
350 for link in subscription.get('links', []):
351 if link['rel'] == 'approve':
352 return {
353 'subscription_id': subscription['id'],
354 'approval_url': link['href']
355 }
356```
357
358## Refund Workflows
359
360```python
361def create_refund(capture_id, amount=None, note=None):
362 """Create a refund for a captured payment."""
363 client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
364
365 url = f"{client.base_url}/v2/payments/captures/{capture_id}/refund"
366 headers = {
367 "Content-Type": "application/json",
368 "Authorization": f"Bearer {client.access_token}"
369 }
370
371 payload = {}
372 if amount:
373 payload["amount"] = {
374 "value": str(amount),
375 "currency_code": "USD"
376 }
377
378 if note:
379 payload["note_to_payer"] = note
380
381 response = requests.post(url, headers=headers, json=payload)
382 return response.json()
383
384def get_refund_details(refund_id):
385 """Get refund details."""
386 client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
387
388 url = f"{client.base_url}/v2/payments/refunds/{refund_id}"
389 headers = {
390 "Authorization": f"Bearer {client.access_token}"
391 }
392
393 response = requests.get(url, headers=headers)
394 return response.json()
395```
396
397## Error Handling
398
399```python
400class PayPalError(Exception):
401 """Custom PayPal error."""
402 pass
403
404def handle_paypal_api_call(api_function):
405 """Wrapper for PayPal API calls with error handling."""
406 try:
407 result = api_function()
408 return result
409 except requests.exceptions.RequestException as e:
410 # Network error
411 raise PayPalError(f"Network error: {str(e)}")
412 except Exception as e:
413 # Other errors
414 raise PayPalError(f"PayPal API error: {str(e)}")
415
416# Usage
417try:
418 order = handle_paypal_api_call(lambda: client.create_order(25.00))
419except PayPalError as e:
420 # Handle error appropriately
421 log_error(e)
422```
423
424## Testing
425
426```python
427# Use sandbox credentials
428SANDBOX_CLIENT_ID = "..."
429SANDBOX_SECRET = "..."
430
431# Test accounts
432# Create test buyer and seller accounts at developer.paypal.com
433
434def test_payment_flow():
435 """Test complete payment flow."""
436 client = PayPalClient(SANDBOX_CLIENT_ID, SANDBOX_SECRET, mode='sandbox')
437
438 # Create order
439 order = client.create_order(10.00)
440 assert 'id' in order
441
442 # Get approval URL
443 approval_url = next((link['href'] for link in order['links'] if link['rel'] == 'approve'), None)
444 assert approval_url is not None
445
446 # After approval (manual step with test account)
447 # Capture order
448 # captured = client.capture_order(order['id'])
449 # assert captured['status'] == 'COMPLETED'
450```
451
452## Resources
453
454- **references/express-checkout.md**: Express Checkout implementation guide
455- **references/ipn-handling.md**: IPN verification and processing
456- **references/refund-workflows.md**: Refund handling patterns
457- **references/billing-agreements.md**: Recurring billing setup
458- **assets/paypal-client.py**: Production PayPal client
459- **assets/ipn-processor.py**: IPN webhook processor
460- **assets/recurring-billing.py**: Subscription management
461
462## Best Practices
463
4641. **Always Verify IPN**: Never trust IPN without verification
4652. **Idempotent Processing**: Handle duplicate IPN notifications
4663. **Error Handling**: Implement robust error handling
4674. **Logging**: Log all transactions and errors
4685. **Test Thoroughly**: Use sandbox extensively
4696. **Webhook Backup**: Don't rely solely on client-side callbacks
4707. **Currency Handling**: Always specify currency explicitly
471
472## Common Pitfalls
473
474- **Not Verifying IPN**: Accepting IPN without verification
475- **Duplicate Processing**: Not checking for duplicate transactions
476- **Wrong Environment**: Mixing sandbox and production URLs/credentials
477- **Missing Webhooks**: Not handling all payment states
478- **Hardcoded Values**: Not making configurable for different environments
479