- 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.
11 KiB
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
- Payment Provider Configuration (stored in database):
api_key: Live API keywebhook_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.
- Webhook URL:
https://your-domain.test/webhook/polar
Polar Dashboard Setup
- Navigate to your Polar.sh dashboard
- Go to Settings → Webhooks
- Add webhook URL:
https://your-domain.test/webhook/polar - Select events to monitor:
subscription.createdsubscription.activesubscription.cancelledsubscription.pausedsubscription.resumedsubscription.trial_will_endsubscription.trial_endedsubscription.uncanceledcustomer.state_changed
Security Features
Signature Validation
The webhook system implements Standard Webhooks specification for Polar signature validation:
-
Headers Expected:
webhook-id: Unique webhook identifierwebhook-signature: HMAC-SHA256 signature in formatv1,<signature>webhook-timestamp: Unix timestamp of request (in seconds)
-
Validation Implementation:
- Uses built-in
App\Services\Webhooks\StandardWebhooksclass - Created via
App\Services\Webhooks\WebhookFactory::createPolar($secret) - Follows official Standard Webhooks specification
- Uses built-in
-
Validation Process:
- Extract required headers:
webhook-id,webhook-timestamp,webhook-signature - Verify all required headers are present
- Validate timestamp is within ±5 minutes (replay attack prevention)
- For Polar: use raw webhook secret (base64 encoded during HMAC operations)
- Construct signed payload:
{webhook-id}.{webhook-timestamp}.{raw-request-body} - Compute HMAC-SHA256 signature and base64 encode result
- Parse received signature format (
v1,<signature>) and compare - Return decoded payload on successful validation
- Extract required headers:
-
Error Handling:
WebhookVerificationExceptionfor validation failuresWebhookSigningExceptionfor 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 classWebhookFactory.php: Factory for creating provider-specific validatorsWebhookVerificationException.php: Validation failure exceptionWebhookSigningException.php: Signing error exception
-
Multi-Provider Support:
// 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 allowedX-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:
- Primary Match:
provider_subscription_id - Fallback Match:
provider_checkout_id(for newly created subscriptions) - Customer Binding: Uses
polar_cust_idfrom 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
PaymentProvidermodel with Polar configuration - Uses
sandbox: falsefor validation tests - Properly sets up webhook secrets and API keys
Running Tests
# 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:
- Verify webhook secret is stored as raw text in database (not base64 encoded)
- Check all required headers are present:
webhook-id,webhook-timestamp,webhook-signature - Ensure headers use Standard Webhooks naming (not Polar-specific headers)
- Verify timestamp is in seconds, not milliseconds
- Check that payment provider configuration has
sandbox: falsefor production - Enable debug logging to see detailed validation steps
- 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:
- Check
provider_subscription_idin database - Verify
provider_checkout_idis set for new subscriptions - Confirm user has correct
polar_cust_id - Check webhook payload contains expected customer/subscription IDs
Rate Limit Exceeded
Symptoms: HTTP 429 responses
Solutions:
- Check if Polar is sending duplicate webhooks
- Verify webhook endpoint isn't being called by other services
- 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:
- Update user records with
polar_cust_id - Create subscription records with
provider = 'polar' - Set proper
provider_subscription_idandprovider_checkout_id - 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:
- Check application logs for detailed error information
- Verify Polar dashboard configuration
- Test with Polar's webhook testing tools
- Review this documentation for common solutions
- Check test suite for expected behavior examples