Droplet Subscription Webhook Guide
This guide explains how to create a droplet that listens to webhooks for subscription management. The droplet will register for subscription creation events and automatically update all customer subscriptions to have the same billing date (15th of the month) for consolidated processing.
Overview
This guide covers:
- Creating a droplet that listens to subscription webhooks
- Registering webhooks for subscription events
- Processing webhook payloads to update subscription billing dates
- Implementing the business logic to synchronize customer subscription billing
Prerequisites
- Access to the Fluid API with droplet creation permissions
- Understanding of webhook handling and authentication
- Basic knowledge of subscription management concepts
Architecture Overview
Step 1: Create the Droplet
First, create a droplet that will handle subscription webhook events:
curl -X POST "https://api.fluid.app/api/droplets" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "droplet": { "name": "Subscription Billing Synchronizer", "embed_url": "https://your-app.com/droplet/subscription-sync", "active": true, "publicly_available": false, "settings": { "marketplace_page": { "title": "Subscription Billing Synchronizer", "summary": "Automatically synchronizes customer subscription billing dates to the 15th of each month", "logo_url": "https://your-app.com/logo.svg" }, "details_page": { "title": "Subscription Billing Synchronizer", "summary": "Keeps all customer subscriptions on the same billing cycle for easier management", "logo": "https://your-app.com/big-logo.svg", "features": [ { "name": "Automatic Synchronization", "summary": "Automatically updates subscription billing dates", "details": "When a new subscription is created, all existing subscriptions for that customer are updated to bill on the 15th of the month" }, { "name": "Consolidated Billing", "summary": "Simplifies billing management", "details": "All customer subscriptions process on the same day, reducing administrative overhead" } ] } } } }'
You can also do this from the Droplet Marketplace and click Create Droplet.
Step 2: Install the Droplet
Once created, install the droplet for your company:
- Navigate to the Droplet Marketplace
- Select your Droplet
- Click on "Install Droplet"
Step 3: Register Webhook for Subscription Events
Once installed, your droplet will receive the droplet installed webhook event. Register a webhook to listen for subscription creation events by using the Droplet Installation Token:
curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "resource": "subscription", "event": "started", "url": "https://your-app.com/webhooks/subscription-sync", "http_method": "post", "auth_token": "your_secure_webhook_token" } }'
Step 4: Implement Webhook Handler
Create a webhook handler that processes subscription events:
class SubscriptionSyncController < ApplicationController skip_before_action :verify_authenticity_token before_action :verify_webhook_auth def handle_subscription_webhook payload = JSON.parse(request.body.read) # Extract subscription data from webhook payload subscription_data = payload.dig('payload', 'subscription_order') return head :ok unless subscription_data # Process the subscription synchronization SubscriptionSynchronizer.new(subscription_data).synchronize_billing_dates head :ok rescue JSON::ParserError head :bad_request rescue StandardError => e Rails.logger.error "Subscription sync error: #{e.message}" head :internal_server_error end private def verify_webhook_auth received_token = request.headers['AUTH_TOKEN'] expected_token = ENV['WEBHOOK_AUTH_TOKEN'] unless ActiveSupport::SecurityUtils.secure_compare(received_token, expected_token) head :unauthorized end end end
Step 5: Create Subscription Synchronizer Service
Implement the business logic to synchronize subscription billing dates:
class SubscriptionSynchronizer BILLING_DAY = 15 def initialize(subscription_data) @subscription_data = subscription_data @customer_id = subscription_data['customer']['id'] @company_id = subscription_data.dig('payload', 'company_id') end def synchronize_billing_dates # Find all active subscriptions for this customer customer_subscriptions = find_customer_subscriptions # Calculate the next 15th of the month next_billing_date = calculate_next_billing_date # Update all subscriptions to bill on the 15th update_subscription_billing_dates(customer_subscriptions, next_billing_date) Rails.logger.info "Synchronized #{customer_subscriptions.count} subscriptions for customer #{@customer_id}" end private def find_customer_subscriptions # Use Fluid API to find all subscriptions for this customer response = HTTParty.get( "https://api.fluid.app/api/company/subscriptions", headers: { 'Authorization' => "Bearer #{ENV['FLUID_API_TOKEN']}", 'Content-Type' => 'application/json' }, query: { customer_id: @customer_id, status: 'active' } ) if response.success? JSON.parse(response.body)['subscriptions'] else Rails.logger.error "Failed to fetch customer subscriptions: #{response.body}" [] end end def calculate_next_billing_date today = Date.current current_month_15th = Date.new(today.year, today.month, BILLING_DAY) if today.day <= BILLING_DAY # If we're before the 15th this month, use this month's 15th current_month_15th else # If we're after the 15th, use next month's 15th current_month_15th.next_month end end def update_subscription_billing_dates(subscriptions, billing_date) subscriptions.each do |subscription| update_single_subscription(subscription['id'], billing_date) end end def update_single_subscription(subscription_id, billing_date) response = HTTParty.patch( "https://api.fluid.app/api/company/subscriptions/#{subscription_id}", headers: { 'Authorization' => "Bearer #{ENV['FLUID_API_TOKEN']}", 'Content-Type' => 'application/json' }, body: { subscription: { next_bill_date: billing_date.strftime('%Y-%m-%d') } }.to_json ) if response.success? Rails.logger.info "Updated subscription #{subscription_id} to bill on #{billing_date}" else Rails.logger.error "Failed to update subscription #{subscription_id}: #{response.body}" end end end
Step 6: Handle Webhook Payload Structure
The webhook payload for subscription events follows this structure:
{ "id": "whe_1234567890abcdef", "identifier": "whe_unique_identifier", "name": "subscription_started", "payload": { "event_name": "subscription_started", "company_id": 1, "resource_name": "SubscriptionOrder", "resource": "subscription", "event": "started", "subscription_order": { "id": 12345, "status": "active", "next_bill_date": "2024-02-01T00:00:00Z", "customer": { "id": 789, "email": "customer@example.com", "first_name": "John", "last_name": "Doe" }, "subscription_plan": { "id": 10, "name": "Monthly Premium", "billing_interval": 1, "billing_interval_unit": "month" } } }, "timestamp": "2024-01-01T12:00:00Z" }
Step 7: Advanced Implementation with Error Handling
Enhance the implementation with proper error handling and logging:
class SubscriptionSynchronizer BILLING_DAY = 15 MAX_RETRIES = 3 def initialize(subscription_data) @subscription_data = subscription_data @customer_id = subscription_data['customer']['id'] @company_id = subscription_data.dig('payload', 'company_id') @new_subscription_id = subscription_data['id'] end def synchronize_billing_dates Rails.logger.info "Starting subscription synchronization for customer #{@customer_id}" begin customer_subscriptions = find_customer_subscriptions if customer_subscriptions.empty? Rails.logger.info "No existing subscriptions found for customer #{@customer_id}" return end next_billing_date = calculate_next_billing_date updated_count = update_subscription_billing_dates(customer_subscriptions, next_billing_date) Rails.logger.info "Successfully synchronized #{updated_count} subscriptions for customer #{@customer_id}" rescue StandardError => e Rails.logger.error "Subscription synchronization failed: #{e.message}" raise end end private def find_customer_subscriptions retry_count = 0 begin response = HTTParty.get( "https://api.fluid.app/api/company/subscriptions", headers: api_headers, query: { customer_id: @customer_id, status: 'active' }, timeout: 30 ) if response.success? subscriptions = JSON.parse(response.body)['subscriptions'] # Exclude the new subscription that triggered this webhook subscriptions.reject { |sub| sub['id'] == @new_subscription_id } else raise "API request failed: #{response.code} - #{response.body}" end rescue StandardError => e retry_count += 1 if retry_count < MAX_RETRIES Rails.logger.warn "Retrying subscription fetch (attempt #{retry_count}): #{e.message}" sleep(2 ** retry_count) # Exponential backoff retry else Rails.logger.error "Failed to fetch customer subscriptions after #{MAX_RETRIES} attempts: #{e.message}" raise end end end def update_subscription_billing_dates(subscriptions, billing_date) updated_count = 0 subscriptions.each do |subscription| if update_single_subscription(subscription['id'], billing_date) updated_count += 1 end end updated_count end def update_single_subscription(subscription_id, billing_date) retry_count = 0 begin response = HTTParty.patch( "https://api.fluid.app/api/company/subscriptions/#{subscription_id}", headers: api_headers, body: { subscription: { next_bill_date: billing_date.strftime('%Y-%m-%d') } }.to_json, timeout: 30 ) if response.success? Rails.logger.info "Updated subscription #{subscription_id} to bill on #{billing_date}" true else Rails.logger.error "Failed to update subscription #{subscription_id}: #{response.body}" false end rescue StandardError => e retry_count += 1 if retry_count < MAX_RETRIES Rails.logger.warn "Retrying subscription update (attempt #{retry_count}): #{e.message}" sleep(2 ** retry_count) retry else Rails.logger.error "Failed to update subscription #{subscription_id} after #{MAX_RETRIES} attempts: #{e.message}" false end end end def api_headers { 'Authorization' => "Bearer #{ENV['FLUID_API_TOKEN']}", 'Content-Type' => 'application/json' } end def calculate_next_billing_date today = Date.current current_month_15th = Date.new(today.year, today.month, BILLING_DAY) if today.day <= BILLING_DAY current_month_15th else current_month_15th.next_month end end end
Step 8: Testing the Implementation
Create tests to verify your implementation:
require 'rails_helper' RSpec.describe SubscriptionSynchronizer do let(:subscription_data) do { 'id' => 12345, 'customer' => { 'id' => 789 }, 'payload' => { 'company_id' => 1 } } end let(:synchronizer) { described_class.new(subscription_data) } describe '#synchronize_billing_dates' do let(:existing_subscriptions) do [ { 'id' => 111, 'status' => 'active' }, { 'id' => 222, 'status' => 'active' } ] end before do allow(synchronizer).to receive(:find_customer_subscriptions) .and_return(existing_subscriptions) allow(synchronizer).to receive(:update_single_subscription) .and_return(true) end it 'synchronizes all customer subscriptions' do expect(synchronizer).to receive(:update_single_subscription) .with(111, kind_of(Date)) expect(synchronizer).to receive(:update_single_subscription) .with(222, kind_of(Date)) synchronizer.synchronize_billing_dates end it 'calculates the correct billing date' do allow(Date).to receive(:current).and_return(Date.new(2024, 1, 10)) synchronizer.synchronize_billing_dates expect(synchronizer).to have_received(:update_single_subscription) .with(111, Date.new(2024, 1, 15)) end end end # Controller test RSpec.describe SubscriptionSyncController, type: :controller do describe 'POST #handle_subscription_webhook' do let(:valid_payload) do { 'payload' => { 'subscription_order' => { 'id' => 12345, 'customer' => { 'id' => 789 } } } } end before do request.headers['AUTH_TOKEN'] = 'valid_token' allow(ENV).to receive(:[]).with('WEBHOOK_AUTH_TOKEN').and_return('valid_token') allow(SubscriptionSynchronizer).to receive(:new).and_return(double(synchronize_billing_dates: true)) end it 'processes valid webhook and returns 200' do post :handle_subscription_webhook, body: valid_payload.to_json expect(response).to have_http_status(:ok) end it 'returns 401 for invalid auth token' do request.headers['AUTH_TOKEN'] = 'invalid_token' post :handle_subscription_webhook, body: valid_payload.to_json expect(response).to have_http_status(:unauthorized) end end end
Step 9: Monitoring and Logging
Implement comprehensive monitoring for your droplet:
class SubscriptionSyncController < ApplicationController around_action :monitor_webhook_processing def handle_subscription_webhook payload = JSON.parse(request.body.read) subscription_id = payload.dig('payload', 'subscription_order', 'id') customer_id = payload.dig('payload', 'subscription_order', 'customer', 'id') # Log webhook receipt Rails.logger.info "Received subscription webhook", subscription_id: subscription_id, customer_id: customer_id # Process the webhook SubscriptionSynchronizer.new(payload.dig('payload', 'subscription_order')).synchronize_billing_dates # Log success Rails.logger.info "Successfully processed subscription webhook", subscription_id: subscription_id, customer_id: customer_id head :ok rescue StandardError => e # Log error with context Rails.logger.error "Subscription webhook processing failed", subscription_id: subscription_id, customer_id: customer_id, error: e.message, backtrace: e.backtrace.first(5) head :internal_server_error end private def monitor_webhook_processing start_time = Time.current yield ensure duration = Time.current - start_time Rails.logger.info "Webhook processing completed", duration: duration # Send metrics to monitoring service if defined?(StatsD) StatsD.timing('webhook.processing_time', duration) StatsD.increment('webhook.subscription_sync.processed') end end end
Step 10: Deployment and Configuration
Environment Variables
Set up the required environment variables:
# Webhook authentication WEBHOOK_AUTH_TOKEN=your_secure_webhook_token # Fluid API access FLUID_API_TOKEN=your_fluid_api_token # Optional: Custom billing day SUBSCRIPTION_BILLING_DAY=15
Routes Configuration
Add the webhook route to your Rails application:
# config/routes.rb Rails.application.routes.draw do post '/webhooks/subscription-sync', to: 'subscription_sync#handle_subscription_webhook' end
Background Job Processing
For better performance, process webhooks asynchronously:
class SubscriptionSyncController < ApplicationController def handle_subscription_webhook payload = JSON.parse(request.body.read) # Queue for background processing SubscriptionSyncJob.perform_later(payload.dig('payload', 'subscription_order')) head :ok end end class SubscriptionSyncJob < ApplicationJob queue_as :subscription_sync def perform(subscription_data) SubscriptionSynchronizer.new(subscription_data).synchronize_billing_dates end end
Best Practices
1. Idempotency
Ensure your webhook handler can process the same event multiple times safely:
class SubscriptionSynchronizer def synchronize_billing_dates # Check if we've already processed this subscription return if already_processed?(@new_subscription_id) # Process the synchronization perform_synchronization # Mark as processed mark_as_processed(@new_subscription_id) end private def already_processed?(subscription_id) ProcessedWebhook.exists?(event_id: subscription_id) end def mark_as_processed(subscription_id) ProcessedWebhook.create!(event_id: subscription_id, processed_at: Time.current) end end
2. Rate Limiting
Implement rate limiting to prevent API abuse:
class SubscriptionSyncController < ApplicationController before_action :check_rate_limit private def check_rate_limit key = "webhook_rate_limit:#{request.remote_ip}" count = Rails.cache.read(key) || 0 if count > 100 # Max 100 requests per hour head :too_many_requests else Rails.cache.write(key, count + 1, expires_in: 1.hour) end end end
3. Error Recovery
Implement retry logic for failed API calls:
class SubscriptionSynchronizer def update_single_subscription(subscription_id, billing_date) retry_count = 0 begin response = HTTParty.patch(api_url(subscription_id), request_options(billing_date)) if response.success? true else raise "API request failed: #{response.code}" end rescue StandardError => e retry_count += 1 if retry_count < MAX_RETRIES sleep(2 ** retry_count) # Exponential backoff retry else Rails.logger.error "Failed to update subscription after #{MAX_RETRIES} attempts" false end end end end
Troubleshooting
Common Issues
Webhook not receiving events
- Verify webhook URL is accessible
- Check authentication token
- Ensure webhook is active
API rate limiting
- Implement exponential backoff
- Use background jobs for processing
- Monitor API usage
Subscription updates failing
- Verify API permissions
- Check subscription status
- Validate date format
Debugging
Enable detailed logging for troubleshooting:
class SubscriptionSynchronizer def synchronize_billing_dates Rails.logger.debug "Starting synchronization for customer #{@customer_id}" customer_subscriptions = find_customer_subscriptions Rails.logger.debug "Found #{customer_subscriptions.count} subscriptions" next_billing_date = calculate_next_billing_date Rails.logger.debug "Next billing date: #{next_billing_date}" # ... rest of implementation end end
Conclusion
This guide provides a complete implementation for creating a droplet that listens to subscription webhooks and synchronizes customer billing dates. The solution includes:
- Droplet creation and installation
- Webhook registration and handling
- Business logic for subscription synchronization
- Error handling and retry mechanisms
- Testing and monitoring
- Best practices for production deployment
By following this guide, you can create a robust droplet that automatically manages subscription billing synchronization, reducing administrative overhead and improving customer experience.
Next Steps
- Test the implementation in a development environment
- Set up monitoring and alerting
- Deploy to production with proper error handling
- Monitor performance and optimize as needed
For additional help, refer to the Fluid API documentation or contact support.