From 703025b57cdf0efce3aca9d6fd10b2bf8681ea6a Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Wed, 4 Feb 2026 13:59:10 +0000 Subject: [PATCH 1/5] fix: improve paddle-webhooks skill --- skills/paddle-webhooks/SKILL.md | 39 ++++++++-- skills/paddle-webhooks/TODO.md | 30 ++++++++ .../examples/express/package.json | 5 +- .../examples/express/src/index.js | 60 ++++++++++----- .../paddle-webhooks/examples/fastapi/main.py | 47 +++++++++--- .../examples/fastapi/requirements.txt | 3 +- .../nextjs/app/webhooks/paddle/route.ts | 74 +++++++++++++------ .../examples/nextjs/package.json | 7 +- .../references/verification.md | 48 +++++++----- 9 files changed, 230 insertions(+), 83 deletions(-) create mode 100644 skills/paddle-webhooks/TODO.md diff --git a/skills/paddle-webhooks/SKILL.md b/skills/paddle-webhooks/SKILL.md index 693657c..fad1ae9 100644 --- a/skills/paddle-webhooks/SKILL.md +++ b/skills/paddle-webhooks/SKILL.md @@ -76,8 +76,14 @@ app.post('/webhooks/paddle', function verifyPaddleSignature(payload, signature, secret) { const parts = signature.split(';'); - const ts = parts.find(p => p.startsWith('ts=')).slice(3); - const h1 = parts.find(p => p.startsWith('h1=')).slice(3); + const ts = parts.find(p => p.startsWith('ts='))?.slice(3); + const signatures = parts + .filter(p => p.startsWith('h1=')) + .map(p => p.slice(3)); + + if (!ts || signatures.length === 0) { + return false; + } const signedPayload = `${ts}:${payload}`; const expectedSignature = crypto @@ -85,9 +91,12 @@ function verifyPaddleSignature(payload, signature, secret) { .update(signedPayload) .digest('hex'); - return crypto.timingSafeEqual( - Buffer.from(h1), - Buffer.from(expectedSignature) + // Check if any signature matches (handles secret rotation) + return signatures.some(sig => + crypto.timingSafeEqual( + Buffer.from(sig), + Buffer.from(expectedSignature) + ) ); } ``` @@ -118,12 +127,26 @@ async def paddle_webhook(request: Request): return {"received": True} def verify_paddle_signature(payload, signature, secret): - parts = dict(p.split('=') for p in signature.split(';')) - signed_payload = f"{parts['ts']}:{payload}" + parts = signature.split(';') + timestamp = None + signatures = [] + + for part in parts: + if part.startswith('ts='): + timestamp = part[3:] + elif part.startswith('h1='): + signatures.append(part[3:]) + + if not timestamp or not signatures: + return False + + signed_payload = f"{timestamp}:{payload}" expected = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() - return hmac.compare_digest(parts['h1'], expected) + + # Check if any signature matches (handles secret rotation) + return any(hmac.compare_digest(sig, expected) for sig in signatures) ``` > **For complete working examples with tests**, see: diff --git a/skills/paddle-webhooks/TODO.md b/skills/paddle-webhooks/TODO.md new file mode 100644 index 0000000..adf683f --- /dev/null +++ b/skills/paddle-webhooks/TODO.md @@ -0,0 +1,30 @@ +# TODO - Known Issues and Improvements + +*Last updated: 2026-02-04* + +These items were identified during automated review but are acceptable for merge. +Contributions to address these items are welcome. + +## Issues + +### Critical + +- [ ] **skills/paddle-webhooks/references/verification.md**: The verification guide includes SDK examples but they don't match the feedback from Paddle team. Need to update SDK verification examples to match Paddle's official SDK middleware approach + - Suggested fix: Update SDK examples in verification.md to use paddle.notifications.verify() and paddle.notifications.unmarshal() methods as shown in the official Paddle SDK documentation + +### Major + +- [ ] **skills/paddle-webhooks/examples/express/src/index.js**: Express example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples + - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples +- [ ] **skills/paddle-webhooks/examples/fastapi/main.py**: FastAPI example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples + - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples +- [ ] **skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts**: Next.js example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples + - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples + +### Minor + +- [ ] **skills/paddle-webhooks/references/verification.md**: The replay protection example shows toleranceSeconds as 5 but the comment incorrectly says it's in seconds when it should clarify it's comparing seconds + - Suggested fix: Update line 184 comment to be clearer about the units being compared +- [ ] **skills/paddle-webhooks/examples/fastapi/requirements.txt**: FastAPI version constraint is >=0.100.0 but latest stable is 0.128.0. Could be more specific + - Suggested fix: Consider using >=0.128.0 for FastAPI to ensure users get the latest stable version + diff --git a/skills/paddle-webhooks/examples/express/package.json b/skills/paddle-webhooks/examples/express/package.json index bb53fab..f8b0a6f 100644 --- a/skills/paddle-webhooks/examples/express/package.json +++ b/skills/paddle-webhooks/examples/express/package.json @@ -8,11 +8,12 @@ "test": "jest" }, "dependencies": { + "@paddle/paddle-node-sdk": "^1.4.0", "dotenv": "^16.3.0", - "express": "^4.18.0" + "express": "^5.2.1" }, "devDependencies": { - "jest": "^29.7.0", + "jest": "^30.2.0", "supertest": "^6.3.0" } } diff --git a/skills/paddle-webhooks/examples/express/src/index.js b/skills/paddle-webhooks/examples/express/src/index.js index b0fc3fb..87450d4 100644 --- a/skills/paddle-webhooks/examples/express/src/index.js +++ b/skills/paddle-webhooks/examples/express/src/index.js @@ -1,9 +1,13 @@ require('dotenv').config(); const express = require('express'); const crypto = require('crypto'); +const { Paddle } = require('@paddle/paddle-node-sdk'); const app = express(); +// Initialize Paddle SDK if API key is available +const paddle = process.env.PADDLE_API_KEY ? new Paddle(process.env.PADDLE_API_KEY) : null; + /** * Verify Paddle webhook signature * @param {string} payload - Raw request body as string @@ -60,26 +64,36 @@ app.post('/webhooks/paddle', async (req, res) => { const signatureHeader = req.headers['paddle-signature']; const payload = req.body.toString(); + const secret = process.env.PADDLE_WEBHOOK_SECRET; - // Verify the webhook signature - const isValid = verifyPaddleSignature( - payload, - signatureHeader, - process.env.PADDLE_WEBHOOK_SECRET - ); + // Option 1: Verify using Paddle SDK (recommended if you have SDK initialized) + let event; + if (paddle) { + try { + // The SDK handles verification and parsing in one step + event = paddle.notifications.unmarshal(payload, signatureHeader, secret); + console.log('Webhook verified using Paddle SDK'); + } catch (err) { + console.error('SDK webhook verification failed:', err.message); + return res.status(400).send('Invalid signature'); + } + } else { + // Option 2: Manual verification (when SDK is not available) + const isValid = verifyPaddleSignature(payload, signatureHeader, secret); - if (!isValid) { - console.error('Webhook signature verification failed'); - return res.status(400).send('Invalid signature'); - } + if (!isValid) { + console.error('Manual webhook signature verification failed'); + return res.status(400).send('Invalid signature'); + } - // Parse the event - let event; - try { - event = JSON.parse(payload); - } catch (err) { - console.error('Failed to parse webhook payload:', err); - return res.status(400).send('Invalid JSON'); + // Parse the event + try { + event = JSON.parse(payload); + console.log('Webhook verified using manual verification'); + } catch (err) { + console.error('Failed to parse webhook payload:', err); + return res.status(400).send('Invalid JSON'); + } } // Handle the event based on type @@ -126,6 +140,18 @@ app.post('/webhooks/paddle', // TODO: Notify customer, update status, etc. break; + case 'customer.created': + const newCustomer = event.data; + console.log('Customer created:', newCustomer.id); + // TODO: Create customer record, send welcome email, etc. + break; + + case 'customer.updated': + const updatedCustomer = event.data; + console.log('Customer updated:', updatedCustomer.id); + // TODO: Update customer record, sync changes, etc. + break; + default: console.log(`Unhandled event type: ${event.event_type}`); } diff --git a/skills/paddle-webhooks/examples/fastapi/main.py b/skills/paddle-webhooks/examples/fastapi/main.py index 020fae1..34bcad4 100644 --- a/skills/paddle-webhooks/examples/fastapi/main.py +++ b/skills/paddle-webhooks/examples/fastapi/main.py @@ -3,6 +3,7 @@ import hashlib from dotenv import load_dotenv from fastapi import FastAPI, Request, HTTPException +from paddle_billing import Client, Environment load_dotenv() @@ -10,6 +11,11 @@ webhook_secret = os.environ.get("PADDLE_WEBHOOK_SECRET") +# Initialize Paddle SDK if API key is available +paddle = None +if os.environ.get("PADDLE_API_KEY"): + paddle = Client(os.environ["PADDLE_API_KEY"]) + def verify_paddle_signature(payload: str, signature_header: str, secret: str) -> bool: """ @@ -66,17 +72,28 @@ async def paddle_webhook(request: Request): if not signature_header: raise HTTPException(status_code=400, detail="Missing Paddle-Signature header") - # Verify the webhook signature - if not verify_paddle_signature(payload_str, signature_header, webhook_secret): - print("Webhook signature verification failed") - raise HTTPException(status_code=400, detail="Invalid signature") - - # Parse the event - try: - event = await request.json() - except Exception as e: - print(f"Failed to parse webhook payload: {e}") - raise HTTPException(status_code=400, detail="Invalid JSON") + # Option 1: Verify using Paddle SDK (recommended if you have SDK initialized) + if paddle: + try: + # The SDK handles verification and parsing in one step + event = paddle.notifications.unmarshal(payload_str, signature_header, webhook_secret) + print("Webhook verified using Paddle SDK") + except Exception as e: + print(f"SDK webhook verification failed: {e}") + raise HTTPException(status_code=400, detail="Invalid signature") + else: + # Option 2: Manual verification (when SDK is not available) + if not verify_paddle_signature(payload_str, signature_header, webhook_secret): + print("Manual webhook signature verification failed") + raise HTTPException(status_code=400, detail="Invalid signature") + + # Parse the event + try: + event = await request.json() + print("Webhook verified using manual verification") + except Exception as e: + print(f"Failed to parse webhook payload: {e}") + raise HTTPException(status_code=400, detail="Invalid JSON") # Handle the event based on type event_type = event.get("event_type") @@ -110,6 +127,14 @@ async def paddle_webhook(request: Request): print(f"Transaction payment failed: {data.get('id')}") # TODO: Notify customer, update status, etc. + elif event_type == "customer.created": + print(f"Customer created: {data.get('id')}") + # TODO: Create customer record, send welcome email, etc. + + elif event_type == "customer.updated": + print(f"Customer updated: {data.get('id')}") + # TODO: Update customer record, sync changes, etc. + else: print(f"Unhandled event type: {event_type}") diff --git a/skills/paddle-webhooks/examples/fastapi/requirements.txt b/skills/paddle-webhooks/examples/fastapi/requirements.txt index 0fe80df..0c00e5f 100644 --- a/skills/paddle-webhooks/examples/fastapi/requirements.txt +++ b/skills/paddle-webhooks/examples/fastapi/requirements.txt @@ -1,5 +1,6 @@ -fastapi>=0.100.0 +fastapi>=0.128.0 uvicorn>=0.23.0 python-dotenv>=1.0.0 +paddle-billing>=0.2.0 pytest>=7.4.0 httpx>=0.25.0 diff --git a/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts b/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts index bb66dbd..64d46a1 100644 --- a/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts +++ b/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts @@ -1,5 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; import crypto from 'crypto'; +import { Paddle } from '@paddle/paddle-node-sdk'; + +// Initialize Paddle SDK if API key is available +const paddle = process.env.PADDLE_API_KEY ? new Paddle(process.env.PADDLE_API_KEY) : null; /** * Verify Paddle webhook signature @@ -55,21 +59,7 @@ export async function POST(request: NextRequest) { // Get the raw body for signature verification const body = await request.text(); const signatureHeader = request.headers.get('paddle-signature'); - - // Verify the webhook signature - const isValid = verifyPaddleSignature( - body, - signatureHeader, - process.env.PADDLE_WEBHOOK_SECRET! - ); - - if (!isValid) { - console.error('Webhook signature verification failed'); - return NextResponse.json( - { error: 'Invalid signature' }, - { status: 400 } - ); - } + const secret = process.env.PADDLE_WEBHOOK_SECRET!; // Parse the event let event: { @@ -79,14 +69,42 @@ export async function POST(request: NextRequest) { data: Record; }; - try { - event = JSON.parse(body); - } catch (err) { - console.error('Failed to parse webhook payload:', err); - return NextResponse.json( - { error: 'Invalid JSON' }, - { status: 400 } - ); + // Option 1: Verify using Paddle SDK (recommended if you have SDK initialized) + if (paddle) { + try { + // The SDK handles verification and parsing in one step + event = paddle.notifications.unmarshal(body, signatureHeader, secret); + console.log('Webhook verified using Paddle SDK'); + } catch (err) { + console.error('SDK webhook verification failed:', err); + return NextResponse.json( + { error: 'Invalid signature' }, + { status: 400 } + ); + } + } else { + // Option 2: Manual verification (when SDK is not available) + const isValid = verifyPaddleSignature(body, signatureHeader, secret); + + if (!isValid) { + console.error('Manual webhook signature verification failed'); + return NextResponse.json( + { error: 'Invalid signature' }, + { status: 400 } + ); + } + + // Parse the event + try { + event = JSON.parse(body); + console.log('Webhook verified using manual verification'); + } catch (err) { + console.error('Failed to parse webhook payload:', err); + return NextResponse.json( + { error: 'Invalid JSON' }, + { status: 400 } + ); + } } // Handle the event based on type @@ -126,6 +144,16 @@ export async function POST(request: NextRequest) { // TODO: Notify customer, update status, etc. break; + case 'customer.created': + console.log('Customer created:', event.data.id); + // TODO: Create customer record, send welcome email, etc. + break; + + case 'customer.updated': + console.log('Customer updated:', event.data.id); + // TODO: Update customer record, sync changes, etc. + break; + default: console.log(`Unhandled event type: ${event.event_type}`); } diff --git a/skills/paddle-webhooks/examples/nextjs/package.json b/skills/paddle-webhooks/examples/nextjs/package.json index 443f8ff..a2a7156 100644 --- a/skills/paddle-webhooks/examples/nextjs/package.json +++ b/skills/paddle-webhooks/examples/nextjs/package.json @@ -9,14 +9,15 @@ "test": "vitest run" }, "dependencies": { - "next": "^14.0.0", + "@paddle/paddle-node-sdk": "^1.4.0", + "next": "^16.1.6", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/node": "^20.0.0", "@types/react": "^18.2.0", - "typescript": "^5.0.0", - "vitest": "^1.0.0" + "typescript": "^5.9.3", + "vitest": "^4.0.18" } } diff --git a/skills/paddle-webhooks/references/verification.md b/skills/paddle-webhooks/references/verification.md index c0dfaa4..6e55364 100644 --- a/skills/paddle-webhooks/references/verification.md +++ b/skills/paddle-webhooks/references/verification.md @@ -30,17 +30,17 @@ const paddle = new Paddle(process.env.PADDLE_API_KEY); app.post('/webhooks/paddle', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['paddle-signature']; const rawBody = req.body.toString(); - + try { - if (!paddle.webhooks.verify(rawBody, signature, process.env.PADDLE_WEBHOOK_SECRET)) { - return res.status(400).send('Invalid signature'); - } - - const event = paddle.webhooks.unmarshal(rawBody, signature, process.env.PADDLE_WEBHOOK_SECRET); + // The SDK handles verification and parsing in one step + const event = paddle.notifications.unmarshal(rawBody, signature, process.env.PADDLE_WEBHOOK_SECRET); + // Handle event... + console.log(`Received event: ${event.event_type}`); res.json({ received: true }); } catch (err) { - res.status(400).send(`Webhook Error: ${err.message}`); + console.error('Webhook verification failed:', err.message); + res.status(400).send('Invalid signature'); } }); ``` @@ -48,6 +48,7 @@ app.post('/webhooks/paddle', express.raw({ type: 'application/json' }), (req, re **Python:** ```python from paddle_billing import Client, Environment +import os paddle = Client(os.environ['PADDLE_API_KEY']) @@ -55,16 +56,18 @@ paddle = Client(os.environ['PADDLE_API_KEY']) async def paddle_webhook(request: Request): payload = await request.body() signature = request.headers.get("paddle-signature") - + webhook_secret = os.environ['PADDLE_WEBHOOK_SECRET'] + try: - if not paddle.notifications.verify(payload.decode(), signature, webhook_secret): - raise HTTPException(status_code=400, detail="Invalid signature") - + # The SDK handles verification and parsing in one step event = paddle.notifications.unmarshal(payload.decode(), signature, webhook_secret) + # Handle event... + print(f"Received event: {event.event_type}") return {"received": True} except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + print(f"Webhook verification failed: {e}") + raise HTTPException(status_code=400, detail="Invalid signature") ``` ### Manual Verification @@ -106,9 +109,18 @@ import hashlib def verify_paddle_signature(payload: str, signature_header: str, secret: str) -> bool: # Parse the signature header - parts = dict(p.split('=', 1) for p in signature_header.split(';')) - timestamp = parts.get('ts') - signature = parts.get('h1') + parts = signature_header.split(';') + timestamp = None + signatures = [] + + for part in parts: + if part.startswith('ts='): + timestamp = part[3:] + elif part.startswith('h1='): + signatures.append(part[3:]) + + if not timestamp or not signatures: + return False # Build the signed payload (timestamp:rawBody) signed_payload = f"{timestamp}:{payload}" @@ -120,8 +132,8 @@ def verify_paddle_signature(payload: str, signature_header: str, secret: str) -> hashlib.sha256 ).hexdigest() - # Use constant-time comparison - return hmac.compare_digest(signature, expected) + # Check if any signature matches (handles secret rotation) + return any(hmac.compare_digest(sig, expected) for sig in signatures) ``` ## Common Gotchas @@ -171,7 +183,7 @@ To prevent replay attacks, check the timestamp (`ts`) against the current time a function isTimestampValid(timestamp, toleranceSeconds = 5) { const now = Math.floor(Date.now() / 1000); const ts = parseInt(timestamp, 10); - return Math.abs(now - ts) <= toleranceSeconds; + return Math.abs(now - ts) <= toleranceSeconds; // Compare the difference in seconds to the tolerance } ``` From 1ddbca3df0bcef866e8efbee51850ad46b38e3b4 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Wed, 4 Feb 2026 16:23:29 +0000 Subject: [PATCH 2/5] fix: correct Paddle SDK verification methods based on official SDK source Based on review of official Paddle SDK source code: - Node.js: paddle.webhooks.unmarshal(body, secret, signature) not paddle.notifications.unmarshal() - Python: Verifier().verify(request, Secret(...)) not paddle.notifications.unmarshal() Changes: - Express: Fix SDK method name and parameter order - Next.js: Fix SDK method name and parameter order - FastAPI: Update to use manual verification (SDK Verifier is Flask/Django specific) - verification.md: Update SDK examples with correct method signatures All tests pass (Express: 10, Next.js: 6, FastAPI: 12). Co-authored-by: Cursor --- skills/paddle-webhooks/TODO.md | 30 ++++------ .../examples/express/src/index.js | 3 +- .../paddle-webhooks/examples/fastapi/main.py | 55 +++++++++++------- .../nextjs/app/webhooks/paddle/route.ts | 11 +++- .../references/verification.md | 56 ++++++++++--------- 5 files changed, 88 insertions(+), 67 deletions(-) diff --git a/skills/paddle-webhooks/TODO.md b/skills/paddle-webhooks/TODO.md index adf683f..14a5d6e 100644 --- a/skills/paddle-webhooks/TODO.md +++ b/skills/paddle-webhooks/TODO.md @@ -2,29 +2,23 @@ *Last updated: 2026-02-04* -These items were identified during automated review but are acceptable for merge. -Contributions to address these items are welcome. +These items were identified during review. Most SDK-related issues have been fixed. -## Issues +## Resolved Issues (2026-02-04) -### Critical +- [x] **SDK verification method**: Fixed to use correct `paddle.webhooks.unmarshal(body, secret, signature)` for Node.js +- [x] **Python SDK pattern**: Updated to use `Verifier().verify(request, Secret(...))` pattern (note: FastAPI uses manual verification) +- [x] **Parameter order**: Fixed Node.js examples to use correct order `(body, secretKey, signature)` +- [x] **Documentation**: Updated verification.md with correct SDK examples -- [ ] **skills/paddle-webhooks/references/verification.md**: The verification guide includes SDK examples but they don't match the feedback from Paddle team. Need to update SDK verification examples to match Paddle's official SDK middleware approach - - Suggested fix: Update SDK examples in verification.md to use paddle.notifications.verify() and paddle.notifications.unmarshal() methods as shown in the official Paddle SDK documentation +## Remaining Items -### Major +### Minor -- [ ] **skills/paddle-webhooks/examples/express/src/index.js**: Express example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples - - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples -- [ ] **skills/paddle-webhooks/examples/fastapi/main.py**: FastAPI example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples - - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples -- [ ] **skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts**: Next.js example correctly implements SDK verification but the manual verification could be improved to match Paddle's official examples - - Suggested fix: Consider updating manual verification implementation to exactly match Paddle's official manual verification examples +- [ ] **FastAPI SDK support**: The Python SDK's `Verifier` class is designed for Flask/Django. Consider adding native FastAPI support in future. +- [ ] **Version constraints**: Consider tightening FastAPI version constraint from `>=0.100.0` to `>=0.128.0` -### Minor +## Feedback for Paddle Team -- [ ] **skills/paddle-webhooks/references/verification.md**: The replay protection example shows toleranceSeconds as 5 but the comment incorrectly says it's in seconds when it should clarify it's comparing seconds - - Suggested fix: Update line 184 comment to be clearer about the units being compared -- [ ] **skills/paddle-webhooks/examples/fastapi/requirements.txt**: FastAPI version constraint is >=0.100.0 but latest stable is 0.128.0. Could be more specific - - Suggested fix: Consider using >=0.128.0 for FastAPI to ensure users get the latest stable version +See `PADDLE_SDK_FEEDBACK.md` for detailed AX/DX feedback to help improve SDK documentation for AI agents and developers. diff --git a/skills/paddle-webhooks/examples/express/src/index.js b/skills/paddle-webhooks/examples/express/src/index.js index 87450d4..9c60fc6 100644 --- a/skills/paddle-webhooks/examples/express/src/index.js +++ b/skills/paddle-webhooks/examples/express/src/index.js @@ -71,7 +71,8 @@ app.post('/webhooks/paddle', if (paddle) { try { // The SDK handles verification and parsing in one step - event = paddle.notifications.unmarshal(payload, signatureHeader, secret); + // Method signature: paddle.webhooks.unmarshal(requestBody, secretKey, signature) + event = await paddle.webhooks.unmarshal(payload, secret, signatureHeader); console.log('Webhook verified using Paddle SDK'); } catch (err) { console.error('SDK webhook verification failed:', err.message); diff --git a/skills/paddle-webhooks/examples/fastapi/main.py b/skills/paddle-webhooks/examples/fastapi/main.py index 34bcad4..dce9f21 100644 --- a/skills/paddle-webhooks/examples/fastapi/main.py +++ b/skills/paddle-webhooks/examples/fastapi/main.py @@ -3,7 +3,6 @@ import hashlib from dotenv import load_dotenv from fastapi import FastAPI, Request, HTTPException -from paddle_billing import Client, Environment load_dotenv() @@ -11,10 +10,14 @@ webhook_secret = os.environ.get("PADDLE_WEBHOOK_SECRET") -# Initialize Paddle SDK if API key is available -paddle = None -if os.environ.get("PADDLE_API_KEY"): - paddle = Client(os.environ["PADDLE_API_KEY"]) +# Initialize Paddle SDK Verifier if available +# The Python SDK uses a Verifier class for webhook signature verification +verifier = None +try: + from paddle_billing.Notifications import Secret, Verifier + verifier = Verifier() +except ImportError: + pass # SDK not installed, use manual verification def verify_paddle_signature(payload: str, signature_header: str, secret: str) -> bool: @@ -72,28 +75,40 @@ async def paddle_webhook(request: Request): if not signature_header: raise HTTPException(status_code=400, detail="Missing Paddle-Signature header") - # Option 1: Verify using Paddle SDK (recommended if you have SDK initialized) - if paddle: + # Option 1: Verify using Paddle SDK (recommended if SDK is available) + # The Python SDK uses Verifier().verify(request, Secret(secret)) pattern + if verifier and webhook_secret: try: - # The SDK handles verification and parsing in one step - event = paddle.notifications.unmarshal(payload_str, signature_header, webhook_secret) - print("Webhook verified using Paddle SDK") - except Exception as e: - print(f"SDK webhook verification failed: {e}") - raise HTTPException(status_code=400, detail="Invalid signature") + # Import Secret for this verification + from paddle_billing.Notifications import Secret + # The SDK's verify() method accepts a request-like object and returns bool + # Note: For FastAPI, we need to create a compatible request object + # Since Verifier expects specific request attributes, we use manual verification + # as the more reliable option for FastAPI + is_valid = verify_paddle_signature(payload_str, signature_header, webhook_secret) + if not is_valid: + print("Webhook signature verification failed") + raise HTTPException(status_code=400, detail="Invalid signature") + print("Webhook verified using manual verification (SDK available but FastAPI requires manual)") + except ImportError: + # Fallback to manual if import fails + if not verify_paddle_signature(payload_str, signature_header, webhook_secret): + print("Manual webhook signature verification failed") + raise HTTPException(status_code=400, detail="Invalid signature") + print("Webhook verified using manual verification") else: # Option 2: Manual verification (when SDK is not available) if not verify_paddle_signature(payload_str, signature_header, webhook_secret): print("Manual webhook signature verification failed") raise HTTPException(status_code=400, detail="Invalid signature") + print("Webhook verified using manual verification") - # Parse the event - try: - event = await request.json() - print("Webhook verified using manual verification") - except Exception as e: - print(f"Failed to parse webhook payload: {e}") - raise HTTPException(status_code=400, detail="Invalid JSON") + # Parse the event + try: + event = await request.json() + except Exception as e: + print(f"Failed to parse webhook payload: {e}") + raise HTTPException(status_code=400, detail="Invalid JSON") # Handle the event based on type event_type = event.get("event_type") diff --git a/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts b/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts index 64d46a1..d0f1973 100644 --- a/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts +++ b/skills/paddle-webhooks/examples/nextjs/app/webhooks/paddle/route.ts @@ -70,10 +70,17 @@ export async function POST(request: NextRequest) { }; // Option 1: Verify using Paddle SDK (recommended if you have SDK initialized) - if (paddle) { + if (paddle && signatureHeader) { try { // The SDK handles verification and parsing in one step - event = paddle.notifications.unmarshal(body, signatureHeader, secret); + // Method signature: paddle.webhooks.unmarshal(requestBody, secretKey, signature) + const sdkEvent = await paddle.webhooks.unmarshal(body, secret, signatureHeader); + event = { + event_id: sdkEvent.eventId, + event_type: sdkEvent.eventType, + occurred_at: sdkEvent.occurredAt, + data: sdkEvent.data as Record, + }; console.log('Webhook verified using Paddle SDK'); } catch (err) { console.error('SDK webhook verification failed:', err); diff --git a/skills/paddle-webhooks/references/verification.md b/skills/paddle-webhooks/references/verification.md index 6e55364..9cc71f9 100644 --- a/skills/paddle-webhooks/references/verification.md +++ b/skills/paddle-webhooks/references/verification.md @@ -20,23 +20,25 @@ The `h1` signature is the current version. There may be multiple `h1` signatures The official Paddle SDKs handle signature verification automatically: -**Node.js:** +**Node.js (`@paddle/paddle-node-sdk` v3.5.0+):** ```javascript -import { Environment, Paddle, EventName } from "@paddle/paddle-node-sdk"; +import { Paddle, EventName } from "@paddle/paddle-node-sdk"; const paddle = new Paddle(process.env.PADDLE_API_KEY); // Express middleware example -app.post('/webhooks/paddle', express.raw({ type: 'application/json' }), (req, res) => { +app.post('/webhooks/paddle', express.raw({ type: 'application/json' }), async (req, res) => { const signature = req.headers['paddle-signature']; const rawBody = req.body.toString(); + const secretKey = process.env.PADDLE_WEBHOOK_SECRET; try { // The SDK handles verification and parsing in one step - const event = paddle.notifications.unmarshal(rawBody, signature, process.env.PADDLE_WEBHOOK_SECRET); + // Method signature: paddle.webhooks.unmarshal(requestBody, secretKey, signature) + const event = await paddle.webhooks.unmarshal(rawBody, secretKey, signature); - // Handle event... - console.log(`Received event: ${event.event_type}`); + // Handle event - note: SDK returns camelCase properties + console.log(`Received event: ${event.eventType}`); res.json({ received: true }); } catch (err) { console.error('Webhook verification failed:', err.message); @@ -45,31 +47,33 @@ app.post('/webhooks/paddle', express.raw({ type: 'application/json' }), (req, re }); ``` -**Python:** -```python -from paddle_billing import Client, Environment -import os +**Python (`paddle-billing` v1.13.0+):** -paddle = Client(os.environ['PADDLE_API_KEY']) +The Python SDK uses a `Verifier` class for webhook signature verification. It supports Flask and Django natively: -@app.post("/webhooks/paddle") -async def paddle_webhook(request: Request): - payload = await request.body() - signature = request.headers.get("paddle-signature") - webhook_secret = os.environ['PADDLE_WEBHOOK_SECRET'] - - try: - # The SDK handles verification and parsing in one step - event = paddle.notifications.unmarshal(payload.decode(), signature, webhook_secret) +```python +from paddle_billing.Notifications import Secret, Verifier - # Handle event... - print(f"Received event: {event.event_type}") - return {"received": True} - except Exception as e: - print(f"Webhook verification failed: {e}") - raise HTTPException(status_code=400, detail="Invalid signature") +# Flask example +@app.route("/webhooks/paddle", methods=["POST"]) +def paddle_webhook(): + webhook_secret = os.environ['PADDLE_WEBHOOK_SECRET'] + + # The Verifier handles signature verification + # Method signature: Verifier().verify(request, Secret(secret)) + is_valid = Verifier().verify(request, Secret(webhook_secret)) + + if not is_valid: + return "Invalid signature", 400 + + # Parse and handle event + event = request.get_json() + print(f"Received event: {event['event_type']}") + return {"received": True} ``` +> **Note for FastAPI users:** The Python SDK's `Verifier` is designed for Flask/Django request objects. For FastAPI, use manual verification (shown below) which is equally secure and more reliable across frameworks. + ### Manual Verification If you need to verify manually: From ad5a0af7c97dad8137da2ece68ce58cacb86dc4a Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Wed, 4 Feb 2026 16:30:53 +0000 Subject: [PATCH 3/5] chore(docs): remove reference to uncommited doc --- skills/paddle-webhooks/TODO.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/skills/paddle-webhooks/TODO.md b/skills/paddle-webhooks/TODO.md index 14a5d6e..9de9fd2 100644 --- a/skills/paddle-webhooks/TODO.md +++ b/skills/paddle-webhooks/TODO.md @@ -18,7 +18,3 @@ These items were identified during review. Most SDK-related issues have been fix - [ ] **FastAPI SDK support**: The Python SDK's `Verifier` class is designed for Flask/Django. Consider adding native FastAPI support in future. - [ ] **Version constraints**: Consider tightening FastAPI version constraint from `>=0.100.0` to `>=0.128.0` -## Feedback for Paddle Team - -See `PADDLE_SDK_FEEDBACK.md` for detailed AX/DX feedback to help improve SDK documentation for AI agents and developers. - From 16c4a3130f317d74797ceaf7039324b4743aa8b3 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 18:33:19 +0000 Subject: [PATCH 4/5] fix: update validate-provider.sh for dynamic test scenarios test-agent-scenario.sh now reads scenarios from providers.yaml instead of hardcoding them. Update validation to check for testScenario in providers.yaml instead of grepping test-agent-scenario.sh. Co-authored-by: Cursor --- scripts/validate-provider.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/validate-provider.sh b/scripts/validate-provider.sh index fd74d43..1892b9b 100755 --- a/scripts/validate-provider.sh +++ b/scripts/validate-provider.sh @@ -212,20 +212,28 @@ validate_integration() { errors+=("$provider not found in README.md Provider Skills table") fi - # Check providers.yaml has entry + # Check providers.yaml has entry with testScenario + # test-agent-scenario.sh now reads scenarios dynamically from providers.yaml if [ -f "$ROOT_DIR/providers.yaml" ]; then if ! grep -q "name: $provider_name" "$ROOT_DIR/providers.yaml"; then errors+=("$provider_name not found in providers.yaml") + else + # Check that the provider has a testScenario defined + # Use awk to find the provider block and check for testScenario + local has_test_scenario + has_test_scenario=$(awk -v provider="$provider_name" ' + /^ - name:/ { in_provider = ($3 == provider) } + in_provider && /testScenario:/ { print "yes"; exit } + /^ - name:/ && !($3 == provider) { in_provider = 0 } + ' "$ROOT_DIR/providers.yaml") + if [ "$has_test_scenario" != "yes" ]; then + errors+=("No testScenario for $provider_name in providers.yaml") + fi fi else errors+=("providers.yaml not found at repository root") fi - # Check test-agent-scenario.sh has at least one scenario - if ! grep -q "$provider_name" "$ROOT_DIR/scripts/test-agent-scenario.sh"; then - errors+=("No scenario for $provider_name in scripts/test-agent-scenario.sh") - fi - # Return errors if [ ${#errors[@]} -gt 0 ]; then printf '%s\n' "${errors[@]}" @@ -324,8 +332,7 @@ if [ ${#FAILED_PROVIDERS[@]} -gt 0 ]; then log "Please ensure you have updated:" log " 1. All required skill files (SKILL.md, references/, examples/)" log " 2. README.md - Add provider to Provider Skills table" - log " 3. providers.yaml - Add provider entry with documentation URLs" - log " 4. scripts/test-agent-scenario.sh - Add at least one test scenario" + log " 3. providers.yaml - Add provider entry with documentation URLs and testScenario" exit 1 else log "${GREEN}All ${#PASSED_PROVIDERS[@]} provider(s) passed validation!${NC}" From b9a30f34f40be74dbe6c9bace8398b812a93c376 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 19:03:40 +0000 Subject: [PATCH 5/5] fix: use correct PyPI package name for Paddle Python SDK Co-authored-by: Cursor --- skills/paddle-webhooks/examples/fastapi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/paddle-webhooks/examples/fastapi/requirements.txt b/skills/paddle-webhooks/examples/fastapi/requirements.txt index 0c00e5f..92153f4 100644 --- a/skills/paddle-webhooks/examples/fastapi/requirements.txt +++ b/skills/paddle-webhooks/examples/fastapi/requirements.txt @@ -1,6 +1,6 @@ fastapi>=0.128.0 uvicorn>=0.23.0 python-dotenv>=1.0.0 -paddle-billing>=0.2.0 +paddle-python-sdk>=1.0.0 pytest>=7.4.0 httpx>=0.25.0