Files
zemailnator/docs/polar-webhooks.md
idevakk 289baa1286 feat(payments): implement standard webhooks validation system
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.
2025-12-06 22:49:54 -08:00

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

  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.

  1. 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:

    // 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

# 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
  • 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