Files
zemailnator/docs/polar-webhooks.md
idevakk 1b438cbf89 feat(webhooks): enhance Polar webhook processing with proper event handling
- Add support for subscription.uncanceled webhook event
  - Fix spelling mismatch for subscription.canceled (Polar) vs subscription.cancelled (code)
  - Implement proper cancel_at_period_end handling in subscription.canceled events
  - Add cancelled_at field updates for subscription.updated events
  - Handle Polar's spelling variants (canceled_at vs cancelled_at) consistently
  - Remove non-existent pause_reason column from subscription uncanceled handler
  - Enhance webhook logging with detailed field update tracking
  - Add comprehensive cancellation metadata storage in provider_data
  - Gracefully handle null provider_subscription_id in payment confirmation polling

  All Polar webhook events now properly sync subscription state including
  cancellation timing, reasons, and billing period details.
2025-12-07 00:57:46 -08:00

330 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`
- `subscription.uncanceled`
- `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
#### `subscription.uncanceled`
- **Trigger**: Previously cancelled subscription is reactivated before cancellation takes effect
- **Action**: Reactivates subscription and clears cancellation details
- **Data**: Subscription ID, new billing period dates, cancellation status cleared
### 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