Add comprehensive webhook validation and processing system with Polar.sh integration: - Create built-in Standard Webhooks package following official specification - Implement HMAC-SHA256 signature validation with base64 encoding - Add webhook factory for multi-provider support (Polar, Stripe, generic) - Replace custom Polar webhook validation with Standard Webhooks implementation - Add proper exception handling with custom WebhookVerificationException - Support sandbox mode bypass for development environments - Update Polar provider to use database-driven configuration - Enhance webhook test suite with proper Standard Webhooks format - Add PaymentProvider model HasFactory trait for testing - Implement timestamp tolerance checking (±5 minutes) for replay protection - Support multiple signature versions and proper header validation This provides a secure, reusable webhook validation system that can be extended to other payment providers while maintaining full compliance with Standard Webhooks specification. BREAKING CHANGE: Polar webhook validation now uses Standard Webhooks format with headers 'webhook-id', 'webhook-timestamp', 'webhook-signature' instead of previous Polar-specific headers.
324 lines
11 KiB
Markdown
324 lines
11 KiB
Markdown
# Polar Webhook Integration Guide
|
|
|
|
## Overview
|
|
|
|
This application supports comprehensive webhook integration with Polar.sh for real-time subscription management and payment processing. The webhook system handles subscription lifecycle events, payment confirmations, and customer status updates.
|
|
|
|
## Configuration
|
|
|
|
### Environment Setup
|
|
|
|
Polar webhook endpoints are automatically configured based on your payment provider settings in the database. The system supports both sandbox and live environments with separate credentials.
|
|
|
|
#### Required Configuration
|
|
|
|
1. **Payment Provider Configuration** (stored in database):
|
|
- `api_key`: Live API key
|
|
- `webhook_secret`: Live webhook secret (stored as raw text)
|
|
- `sandbox_api_key`: Sandbox API key (if sandbox mode enabled)
|
|
- `sandbox_webhook_secret`: Sandbox webhook secret (if sandbox mode enabled, stored as raw text)
|
|
- `sandbox`: Boolean flag indicating sandbox mode
|
|
|
|
**Important**: Store webhook secrets as **raw text** in the database. The system automatically base64 encodes the secret during signature validation as required by Polar's Standard Webhooks implementation.
|
|
|
|
2. **Webhook URL**: `https://your-domain.test/webhook/polar`
|
|
|
|
### Polar Dashboard Setup
|
|
|
|
1. Navigate to your Polar.sh dashboard
|
|
2. Go to Settings → Webhooks
|
|
3. Add webhook URL: `https://your-domain.test/webhook/polar`
|
|
4. Select events to monitor:
|
|
- `subscription.created`
|
|
- `subscription.active`
|
|
- `subscription.cancelled`
|
|
- `subscription.paused`
|
|
- `subscription.resumed`
|
|
- `subscription.trial_will_end`
|
|
- `subscription.trial_ended`
|
|
- `customer.state_changed`
|
|
|
|
## Security Features
|
|
|
|
### Signature Validation
|
|
|
|
The webhook system implements Standard Webhooks specification for Polar signature validation:
|
|
|
|
- **Headers Expected**:
|
|
- `webhook-id`: Unique webhook identifier
|
|
- `webhook-signature`: HMAC-SHA256 signature in format `v1,<signature>`
|
|
- `webhook-timestamp`: Unix timestamp of request (in seconds)
|
|
|
|
- **Validation Implementation**:
|
|
- Uses built-in `App\Services\Webhooks\StandardWebhooks` class
|
|
- Created via `App\Services\Webhooks\WebhookFactory::createPolar($secret)`
|
|
- Follows official Standard Webhooks specification
|
|
|
|
- **Validation Process**:
|
|
1. Extract required headers: `webhook-id`, `webhook-timestamp`, `webhook-signature`
|
|
2. Verify all required headers are present
|
|
3. Validate timestamp is within ±5 minutes (replay attack prevention)
|
|
4. For Polar: use raw webhook secret (base64 encoded during HMAC operations)
|
|
5. Construct signed payload: `{webhook-id}.{webhook-timestamp}.{raw-request-body}`
|
|
6. Compute HMAC-SHA256 signature and base64 encode result
|
|
7. Parse received signature format (`v1,<signature>`) and compare
|
|
8. Return decoded payload on successful validation
|
|
|
|
- **Error Handling**:
|
|
- `WebhookVerificationException` for validation failures
|
|
- `WebhookSigningException` for signing errors
|
|
- Detailed logging for debugging and monitoring
|
|
|
|
### Standard Webhooks Implementation
|
|
|
|
The application includes a built-in Standard Webhooks validation system:
|
|
|
|
- **Location**: `app/Services/Webhooks/`
|
|
- **Components**:
|
|
- `StandardWebhooks.php`: Main validation class
|
|
- `WebhookFactory.php`: Factory for creating provider-specific validators
|
|
- `WebhookVerificationException.php`: Validation failure exception
|
|
- `WebhookSigningException.php`: Signing error exception
|
|
|
|
- **Multi-Provider Support**:
|
|
```php
|
|
// Polar (uses raw secret)
|
|
$webhook = WebhookFactory::createPolar($secret);
|
|
|
|
// Stripe (uses whsec_ prefix)
|
|
$webhook = WebhookFactory::createStripe($secret);
|
|
|
|
// Generic providers
|
|
$webhook = WebhookFactory::create($secret, $isRaw);
|
|
```
|
|
|
|
- **Benefits**:
|
|
- Industry-standard webhook validation
|
|
- Reusable across multiple payment providers
|
|
- Built-in security features (timestamp tolerance, replay prevention)
|
|
- Proper exception handling and logging
|
|
- Testable and maintainable code
|
|
|
|
### Rate Limiting
|
|
|
|
Webhook endpoints are rate-limited to prevent abuse:
|
|
- **Polar**: 60 requests per minute per IP
|
|
- **Other providers**: 30-100 requests per minute depending on provider
|
|
|
|
Rate limit headers are included in responses:
|
|
- `X-RateLimit-Limit`: Maximum requests allowed
|
|
- `X-RateLimit-Remaining`: Requests remaining in current window
|
|
|
|
### Idempotency
|
|
|
|
Webhook processing is idempotent to prevent duplicate processing:
|
|
- Each webhook has a unique ID
|
|
- Processed webhook IDs are cached for 24 hours
|
|
- Duplicate webhooks are ignored but return success response
|
|
|
|
## Supported Events
|
|
|
|
### Subscription Events
|
|
|
|
#### `subscription.created`
|
|
- **Trigger**: New subscription created
|
|
- **Action**: Creates or updates subscription record
|
|
- **Data**: Customer ID, subscription ID, product details, status
|
|
|
|
#### `subscription.active`
|
|
- **Trigger**: Subscription becomes active
|
|
- **Action**: Updates subscription status to active
|
|
- **Data**: Subscription ID, billing period dates, status
|
|
|
|
#### `subscription.cancelled`
|
|
- **Trigger**: Subscription cancelled
|
|
- **Action**: Updates subscription with cancellation details
|
|
- **Data**: Cancellation reason, comments, effective date
|
|
|
|
#### `subscription.paused`
|
|
- **Trigger**: Subscription paused
|
|
- **Action**: Updates subscription status to paused
|
|
- **Data**: Pause reason, pause date
|
|
|
|
#### `subscription.resumed`
|
|
- **Trigger**: Paused subscription resumed
|
|
- **Action**: Updates subscription status to active
|
|
- **Data**: Resume reason, resume date, new billing period
|
|
|
|
#### `subscription.trial_will_end`
|
|
- **Trigger**: Trial period ending soon
|
|
- **Action**: Updates trial end date
|
|
- **Data**: Trial end date
|
|
|
|
#### `subscription.trial_ended`
|
|
- **Trigger**: Trial period ended
|
|
- **Action**: Converts trial to active subscription
|
|
- **Data**: Trial end date, new billing period
|
|
|
|
### Customer Events
|
|
|
|
#### `customer.state_changed`
|
|
- **Trigger**: Customer profile or subscription state changes
|
|
- **Action**: Updates user's Polar customer ID and syncs active subscriptions
|
|
- **Data**: Customer details, active subscriptions list, metadata
|
|
- **Headers**: `webhook-signature`, `webhook-timestamp`, `webhook-id`
|
|
|
|
## Subscription Lookup Logic
|
|
|
|
The webhook handler uses a sophisticated lookup system to find subscriptions:
|
|
|
|
1. **Primary Match**: `provider_subscription_id`
|
|
2. **Fallback Match**: `provider_checkout_id` (for newly created subscriptions)
|
|
3. **Customer Binding**: Uses `polar_cust_id` from user record
|
|
|
|
This ensures webhooks are processed correctly even when the subscription ID hasn't been populated yet.
|
|
|
|
## Error Handling
|
|
|
|
### Validation Failures
|
|
- Invalid signatures: HTTP 400
|
|
- Missing timestamps: HTTP 400
|
|
- Old/future timestamps: HTTP 400
|
|
|
|
### Processing Errors
|
|
- Malformed payloads: Logged and returns HTTP 200
|
|
- Missing subscriptions: Logged and returns HTTP 200
|
|
- Database errors: Logged and returns HTTP 200
|
|
|
|
### Logging
|
|
All webhook activity is logged with:
|
|
- Webhook ID and event type
|
|
- Subscription match details
|
|
- Processing errors
|
|
- Security violations (rate limits, invalid signatures)
|
|
|
|
## Testing
|
|
|
|
### Test Suite
|
|
Comprehensive test suite located at `tests/Feature/Controllers/PolarWebhookTest.php` with 16 test cases covering:
|
|
|
|
- Standard Webhooks signature validation
|
|
- Timestamp validation (±5 minutes tolerance)
|
|
- Header validation (webhook-id, webhook-timestamp, webhook-signature)
|
|
- Idempotency (duplicate webhook prevention)
|
|
- All event handlers (subscription.*, customer.state_changed)
|
|
- Error scenarios and edge cases
|
|
- Sandbox mode bypass functionality
|
|
|
|
### Test Configuration
|
|
Tests use database-driven configuration:
|
|
- Creates `PaymentProvider` model with Polar configuration
|
|
- Uses `sandbox: false` for validation tests
|
|
- Properly sets up webhook secrets and API keys
|
|
|
|
### Running Tests
|
|
```bash
|
|
# Run all Polar webhook tests
|
|
php artisan test tests/Feature/Controllers/PolarWebhookTest.php
|
|
|
|
# Run specific test
|
|
php artisan test --filter="it_processes_valid_webhook_with_correct_signature"
|
|
```
|
|
|
|
### Test Data
|
|
Tests use realistic Polar webhook payloads with:
|
|
- Proper Standard Webhooks signature generation
|
|
- Correct header names and formats
|
|
- Database configuration setup
|
|
- Comprehensive error scenarios
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### Webhook Validation Failing
|
|
**Symptoms**: HTTP 400 responses, `WebhookVerificationException` in logs
|
|
|
|
**Solutions**:
|
|
1. Verify webhook secret is stored as raw text in database (not base64 encoded)
|
|
2. Check all required headers are present: `webhook-id`, `webhook-timestamp`, `webhook-signature`
|
|
3. Ensure headers use Standard Webhooks naming (not Polar-specific headers)
|
|
4. Verify timestamp is in seconds, not milliseconds
|
|
5. Check that payment provider configuration has `sandbox: false` for production
|
|
6. Enable debug logging to see detailed validation steps
|
|
7. Test signature generation using Standard Webhooks format
|
|
|
|
**Debug Mode**:
|
|
Enable detailed webhook logging by setting `LOG_LEVEL=debug` in your environment file. This will provide:
|
|
- Detailed signature validation steps
|
|
- Header parsing information
|
|
- Secret encoding details
|
|
- Payload construction information
|
|
|
|
#### Subscription Not Found
|
|
**Symptoms**: Logs show "No subscription found" warnings
|
|
|
|
**Solutions**:
|
|
1. Check `provider_subscription_id` in database
|
|
2. Verify `provider_checkout_id` is set for new subscriptions
|
|
3. Confirm user has correct `polar_cust_id`
|
|
4. Check webhook payload contains expected customer/subscription IDs
|
|
|
|
#### Rate Limit Exceeded
|
|
**Symptoms**: HTTP 429 responses
|
|
|
|
**Solutions**:
|
|
1. Check if Polar is sending duplicate webhooks
|
|
2. Verify webhook endpoint isn't being called by other services
|
|
3. Monitor rate limit headers in responses
|
|
|
|
### Debug Mode
|
|
Enable detailed webhook logging by setting `LOG_LEVEL=debug` in your environment file. This will provide:
|
|
- Detailed signature validation steps
|
|
- Header parsing information
|
|
- Subscription lookup details
|
|
- Payload parsing information
|
|
|
|
## Monitoring
|
|
|
|
### Key Metrics to Monitor
|
|
- Webhook success rate
|
|
- Validation failure frequency
|
|
- Processing errors by type
|
|
- Rate limit violations
|
|
- Subscription match success rate
|
|
|
|
### Recommended Alerts
|
|
- High validation failure rate (>5%)
|
|
- Rate limit violations
|
|
- Processing errors for critical events (subscription.cancelled)
|
|
- Webhook endpoint downtime
|
|
|
|
## Migration from Other Providers
|
|
|
|
The webhook system is designed to handle multiple payment providers. When migrating to Polar:
|
|
|
|
1. Update user records with `polar_cust_id`
|
|
2. Create subscription records with `provider = 'polar'`
|
|
3. Set proper `provider_subscription_id` and `provider_checkout_id`
|
|
4. Test webhook processing in sandbox mode
|
|
|
|
## Security Considerations
|
|
|
|
- Webhook endpoints bypass CSRF protection but maintain signature validation
|
|
- All webhook processing is logged for audit trails
|
|
- Rate limiting prevents abuse and DoS attacks
|
|
- Idempotency prevents duplicate processing
|
|
- Timestamp validation prevents replay attacks
|
|
|
|
## Performance Considerations
|
|
|
|
- Webhook processing is optimized for speed with minimal database queries
|
|
- Cache-based idempotency checking
|
|
- Efficient subscription lookup with fallback strategies
|
|
- Background processing for heavy operations (if needed)
|
|
|
|
## Support
|
|
|
|
For issues with Polar webhook integration:
|
|
|
|
1. Check application logs for detailed error information
|
|
2. Verify Polar dashboard configuration
|
|
3. Test with Polar's webhook testing tools
|
|
4. Review this documentation for common solutions
|
|
5. Check test suite for expected behavior examples |