Last updated

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

CustomerFluid PlatformDropletWebhook EndpointDatabaseCreates new subscriptionTriggers subscription_started webhookHTTP POST with subscription dataProcess webhook payloadLook up all customer subscriptionsUpdate bill_date to 15th for all subscriptionsConfirm processingCustomerFluid PlatformDropletWebhook EndpointDatabase

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:

  1. Navigate to the Droplet Marketplace
  2. Select your Droplet
  3. 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

  1. Webhook not receiving events

    • Verify webhook URL is accessible
    • Check authentication token
    • Ensure webhook is active
  2. API rate limiting

    • Implement exponential backoff
    • Use background jobs for processing
    • Monitor API usage
  3. 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.