Webhooks Guide
Welcome to the comprehensive guide for using webhooks in the Fluid platform. Webhooks provide a powerful way to receive real-time notifications when events occur in your Fluid commerce environment, enabling you to build responsive integrations and automate business processes.
Introduction to Webhooks in Fluid
What are Webhooks?
Webhooks are HTTP callbacks that Fluid sends to your application when specific events occur. Instead of continuously polling our API for changes, webhooks allow your system to receive instant notifications, making your integrations more efficient and responsive.
Why Use Webhooks?
- Real-time Updates: Receive instant notifications when events occur
- Reduced API Calls: Eliminate the need for constant polling
- Automated Workflows: Trigger business processes automatically
- Better User Experience: Provide immediate feedback to your customers
- Resource Efficiency: Lower server load and improved performance
Common Use Cases
- Order Processing: Automatically fulfill orders, send confirmation emails, or update inventory
- Customer Management: Sync customer data with CRM systems or trigger marketing campaigns
- Inventory Tracking: Update stock levels across multiple sales channels
- Analytics: Feed real-time data into business intelligence tools
- Notifications: Send SMS, email, or push notifications to customers and staff
How Webhooks Work in Fluid
When an event occurs in your Fluid environment (such as a new order being created), the platform automatically sends an HTTP request to your configured webhook endpoint with relevant data about the event.
Creating and Configuring Webhooks
Setting Up Webhooks via API
Webhooks are managed through the Fluid Company API. Here's how to create and configure them:
Creating a Webhook
curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resource": "order", "event": "created", "url": "https://your-app.com/webhooks/orders" }'
Required Parameters
- resource: The type of resource to monitor (e.g., "order", "cart", "customer")
- event: The specific event to listen for (e.g., "created", "updated", "completed")
- url: Your endpoint URL that will receive the webhook calls
Optional Parameters
- http_method: HTTP method to use (default: "post")
- auth_token: Custom authentication token for webhook verification
- synchronous: Whether to call webhook synchronously (default: false)
Response
{ "webhook": { "id": 123, "company_id": 1, "resource": "order", "event": "created", "event_identifier": "order_created", "url": "https://your-app.com/webhooks/orders", "http_method": "post", "active": true, "auth_token": "abc123def456", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } }
Managing Existing Webhooks
List All Webhooks
curl -X GET "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN"
Update a Webhook
curl -X PUT "https://api.fluid.app/api/company/webhooks/123" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "url": "https://your-app.com/new-webhook-endpoint", "auth_token": "new-auth-token" } }'
Delete a Webhook
curl -X DELETE "https://api.fluid.app/api/company/webhooks/123" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN"
Configuration Best Practices
- Use HTTPS: Always use secure HTTPS endpoints for webhook URLs
- Validate URLs: Ensure your webhook endpoints are accessible and return proper HTTP status codes
- Set Timeouts: Configure appropriate timeout values for your webhook endpoints
- Handle Failures: Implement proper error handling and retry logic
Available Webhook Events
Fluid supports webhooks for multiple resource types. Each resource has specific events that can trigger webhook calls.
Complete Event Reference
Commerce Events
Cart Events
- cart_updated: Triggered when cart contents or details are modified. Uses CartBlueprinter with :for_webhook_payload view
- cart_abandoned: Fired when a cart is considered abandoned based on company settings
- cart_update_address: Called when shipping or billing address is updated
- cart_update_cart_email: Triggered when the cart email is changed
- cart_add_items: Fired when items are added to the cart
- cart_remove_items: Called when items are removed from the cart
Order Events
- order_created: Triggered when a new order is placed. Uses OrderBlueprinter with :for_webhook_payload view
- order_updated: Fired when order details are modified. Uses OrderBlueprinter with :for_webhook_payload view
- order_completed: Called when an order is marked as delivered/completed
- order_shipped: Triggered when an order is marked as shipped
- order_cancelled: Fired when an order is cancelled
- order_refunded: Called when a refund is processed for an order
Product Events
- product_created: Triggered when a new product is added. Uses default Product serialization
- product_updated: Fired when product details are modified. Uses default Product serialization
- product_destroyed: Called when a product is deleted. Uses default Product serialization
Subscription Events
- subscription_started: Triggered when a new subscription begins. Uses OrderSerializer with custom_key "subscription_order"
- subscription_paused: Fired when a subscription is paused. Uses OrderSerializer with custom_key "subscription_order"
- subscription_cancelled: Called when a subscription is cancelled. Uses OrderSerializer with custom_key "subscription_order"
Customer & Contact Events
- customer_created: New customer account created
- customer_updated: Customer information modified
- contact_created: New contact/lead added
- contact_updated: Contact information updated
System Events
- user_created: New user account created
- user_updated: User information modified
- user_deactivated: User account deactivated
- enrollment_completed: User enrollment process finished
- event_created: Custom event created
- event_updated: Custom event modified
- event_deleted: Custom event removed
Integration Events
- droplet_installed: Droplet/app installed
- droplet_uninstalled: Droplet/app removed
- bot_message_created: Bot message sent
- popup_submitted: Popup form submitted
- webchat_submitted: Web chat form submitted
Event Naming Convention
All webhook events follow the pattern: {resource}_{event}
- Resource names are singular (e.g., "order", not "orders")
- Event names use past tense (e.g., "created", not "create")
- Multiple words are separated by underscores
Sample Payloads
Order Event Payloads
When order events are triggered, Fluid sends detailed information about the order. Here are examples of the JSON payloads you'll receive:
Order Created/Updated Payload
{ "id": "whe_1234567890abcdef", "identifier": "whe_unique_identifier", "name": "order_created", "payload": { "event_name": "order_created", "company_id": 1, "resource_name": "Commerce::Order", "resource": "order", "event": "created", "order": { "subtotal": 99.99, "tax": 8.50, "discount": 10.00, "shipping": 5.99, "external_id": "ext_order_123", "metadata": { "source": "web", "campaign": "summer_sale" }, "order_class": "customer_order", "rep": { "id": 456, "external_id": "rep_789" }, "items": [ { "id": 1, "product_id": 100, "variant_id": 200, "quantity": 2, "price": 49.99, "total": 99.98, "sku": "PROD-001", "title": "Premium Widget", "variant_title": "Blue - Large" } ], "ship_to": { "id": 301, "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "address2": "Apt 4B", "city": "New York", "state": "NY", "zip": "10001", "country": "US", "phone": "+1-555-123-4567" }, "bill_to": { "id": 302, "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US" } } }, "timestamp": "2024-01-01T12:00:00Z" }
Order Status Change Payloads
When an order status changes, specific event identifiers are used:
Order Shipped (order_shipped
):
{ "payload": { "event_name": "order_shipped", "order": { "id": 12345, "status": "shipped", "tracking_number": "1Z999AA1234567890", "shipped_at": "2024-01-02T10:30:00Z" } } }
Order Completed (order_completed
):
{ "payload": { "event_name": "order_completed", "order": { "id": 12345, "status": "delivered", "delivered_at": "2024-01-05T14:20:00Z" } } }
Order Cancelled (order_cancelled
):
{ "payload": { "event_name": "order_cancelled", "order": { "id": 12345, "status": "cancelled", "cancelled_at": "2024-01-01T15:45:00Z", "cancellation_reason": "Customer request" } } }
Cart Event Payloads
Cart webhooks provide information about shopping cart changes based on the CartBlueprinter :for_webhook_payload view:
Cart Updated Payload
{ "id": "whe_1234567890abcdef", "identifier": "whe_unique_identifier", "name": "cart_updated", "payload": { "event_name": "cart_updated", "company_id": 1, "resource_name": "Commerce::Cart", "resource": "cart", "event": "updated", "data": { "id": 67890, "state": "start", "payment_method": null, "amount_total": 170.72, "sub_total": 149.98, "tax_total": 12.75, "shipping_total": 7.99, "discount_total": 0.00, "currency_code": "USD", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "phone": "+1-555-123-4567", "items": [ { "id": 1, "title": "Premium Widget", "quantity": 2, "price": 74.99, "total": 149.98, "sku": "WIDGET-001", "tax": 6.38, "cv": 50.0, "qv": 40.0, "product": { "id": 100, "title": "Premium Widget", "sku": "WIDGET-001", "price": 74.99 }, "variant": { "id": 200, "title": "Blue - Large", "sku": "WIDGET-001-BL" } } ], "ship_to": { "id": 301, "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US" }, "bill_to": { "id": 302, "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US" } } }, "timestamp": "2024-01-01T11:30:00Z" }
Cart Abandoned Payload
{ "payload": { "event_name": "cart_abandoned", "data": { "id": 67890, "email": "customer@example.com", "amount_total": 170.72, "items_count": 2, "abandoned_at": "2024-01-01T11:30:00Z" } } }
Cart Add Items Payload
{ "payload": { "event_name": "cart_add_items", "data": { "id": 67890, "items_added": [ { "id": 2, "title": "New Product", "quantity": 1, "price": 25.00, "sku": "NEW-001" } ], "amount_total": 195.72, "items_count": 3 } } }
Cart Remove Items Payload
{ "payload": { "event_name": "cart_remove_items", "data": { "id": 67890, "items_removed": [ { "id": 1, "title": "Removed Product", "quantity": 1, "sku": "REM-001" } ], "amount_total": 145.72, "items_count": 2 } } }
Customer Event Payloads
Customer webhooks include comprehensive customer profile information based on the CustomerBlueprinter:
Customer Created Payload
{ "id": "whe_1234567890abcdef", "identifier": "whe_unique_identifier", "name": "customer_created", "payload": { "event_name": "customer_created", "company_id": 1, "resource_name": "Customer", "resource": "customer", "event": "created", "data": { "id": 789, "email": "newcustomer@example.com", "first_name": "Jane", "last_name": "Smith", "phone": "+1-555-987-6543", "orders_count": 0, "total_spent": 0.0, "subscription_count": 0, "is_rep": false, "created_at": "2024-01-01T09:15:00Z", "updated_at": "2024-01-01T09:15:00Z", "addresses": [ { "id": 401, "first_name": "Jane", "last_name": "Smith", "address1": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "zip": "90210", "country": "US", "phone": "+1-555-987-6543" } ], "customer_notes": [], "payment_methods": [] } }, "timestamp": "2024-01-01T09:15:00Z" }
Customer Updated Payload
{ "payload": { "event_name": "customer_updated", "data": { "id": 789, "email": "jane.smith.updated@example.com", "first_name": "Jane", "last_name": "Smith-Johnson", "phone": "+1-555-987-6543", "orders_count": 3, "total_spent": 299.97, "subscription_count": 1, "updated_at": "2024-01-02T14:30:00Z" } } }
Subscription Event Payloads
Subscription webhooks provide information about subscription lifecycle events using OrderSerializer with custom_key "subscription_order":
Subscription Started Payload
{ "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", "last_bill_date": null, "next_ship_date": "2024-02-01T00:00:00Z", "last_ship_date": null, "quantity": 1, "price": 29.99, "original_price": 34.99, "attempts": 0, "skipped_count": 0, "max_skips": 3, "trial_ends_at": "2024-01-15T00:00:00Z", "in_trial": true, "disabled": false, "notes": "Premium subscription for customer", "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-01T12:00:00Z", "subscription_plan": { "id": 10, "name": "Monthly Premium", "billing_interval": 1, "billing_interval_unit": "month", "price_adjustment_amount": 5.00, "price_adjustment_type": "discount" }, "customer": { "id": 789, "email": "customer@example.com", "first_name": "John", "last_name": "Doe" }, "variant": { "id": 200, "title": "Premium Subscription", "sku": "SUB-PREM-001" }, "address": { "id": 301, "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "city": "New York", "state": "NY", "zip": "10001", "country": "US" } } }, "timestamp": "2024-01-01T12:00:00Z" }
Subscription Paused Payload
{ "payload": { "event_name": "subscription_paused", "subscription_order": { "id": 12345, "status": "paused", "next_bill_date": "2024-03-01T00:00:00Z", "last_bill_date": "2024-01-01T00:00:00Z", "paused_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } }
Subscription Cancelled Payload
{ "payload": { "event_name": "subscription_cancelled", "subscription_order": { "id": 12345, "status": "cancelled", "cancelled_at": "2024-01-20T16:45:00Z", "updated_at": "2024-01-20T16:45:00Z", "cancellation_reason": "customer_request" } } }
Product Event Payloads
Product webhooks provide information about product lifecycle events using default Product serialization:
Product Created Payload
{ "id": "whe_1234567890abcdef", "identifier": "whe_unique_identifier", "name": "product_created", "payload": { "event_name": "product_created", "company_id": 1, "resource_name": "Product", "resource": "product", "event": "created", "data": { "id": 100, "title": "Premium Widget", "description": "High-quality widget for premium customers", "introduction": "Introducing our latest premium widget", "feature_text": "Advanced features for professional use", "slug": "premium-widget", "status": "active", "sku": "WIDGET-001", "upc": "123456789012", "hs_code": "8543709099", "image_url": "https://cdn.fluid.app/products/widget.jpg", "compressed_image_url": "https://cdn.fluid.app/products/widget_compressed.jpg", "image_path": "/products/widget.jpg", "external_url": "https://example.com/widget", "tax_category_id": 1, "public": true, "in_stock": true, "keep_selling": true, "track_quantity": true, "no_index": false, "commission": 10.0, "affiliate_commission": 5.0, "integration_id": "shopify_123", "external_id": "ext_prod_123", "last_synced_at": "2024-01-01T10:00:00Z", "metadata": { "source": "manual", "category": "widgets" }, "publish_to_retail_store": true, "publish_to_rep_store": true, "publish_to_share_tab": true, "show_reviews": true, "international_tax_type": "standard", "created_at": "2024-01-01T10:00:00Z", "updated_at": "2024-01-01T10:00:00Z", "category": { "id": 5, "title": "Widgets", "description": "Premium widget collection", "image_url": "https://cdn.fluid.app/categories/widgets.jpg", "position": 1, "public": true }, "collections": [ { "id": 10, "title": "Premium Collection", "description": "Our premium product line", "position": 1, "public": true } ], "tags": [ { "id": 1, "name": "premium" }, { "id": 2, "name": "widget" } ], "label": { "id": 1, "title": "New", "background": "#ff0000", "icon": "star", "color": "#ffffff" }, "images": [ { "id": 1, "image_url": "https://cdn.fluid.app/products/widget_1.jpg", "image_path": "/products/widget_1.jpg", "position": 1 } ], "variants": [ { "id": 200, "title": "Blue - Large", "sku": "WIDGET-001-BL", "price": 74.99, "position": 1, "is_master": true } ] } }, "timestamp": "2024-01-01T10:00:00Z" }
Product Updated Payload
{ "payload": { "event_name": "product_updated", "data": { "id": 100, "title": "Premium Widget - Updated", "description": "Updated high-quality widget for premium customers", "price": 79.99, "status": "active", "updated_at": "2024-01-02T15:30:00Z" } } }
Product Destroyed Payload
{ "payload": { "event_name": "product_destroyed", "data": { "id": 100, "title": "Premium Widget", "sku": "WIDGET-001", "status": "discarded", "destroyed_at": "2024-01-03T09:15:00Z" } } }
Understanding Payload Structure
All webhook payloads follow a consistent structure:
- id: Unique webhook event ID
- identifier: Unique event identifier for tracking
- name: Event name (matches event_identifier)
- payload: Contains the actual event data
- event_name: The specific event that occurred
- company_id: Your company ID
- resource_name: Full class name of the resource
- resource: Resource type (order, cart, customer, etc.)
- event: Event type (created, updated, etc.)
- [resource]: The actual resource data
- timestamp: ISO 8601 timestamp when the event occurred
Authentication and Security
Webhook Authentication
Fluid uses token-based authentication to secure webhook calls. Every webhook request includes authentication headers that you should verify to ensure the request is legitimate.
Authentication Headers
Each webhook request includes the following headers:
POST /your-webhook-endpoint HTTP/1.1 Host: your-app.com Content-Type: application/json AUTH_TOKEN: your_webhook_auth_token X-Fluid-Shop: your-company-subdomain
Header Descriptions
- AUTH_TOKEN: Your webhook verification token
- X-Fluid-Shop: Your company's Fluid subdomain identifier
- Content-Type: Always
application/json
Verification Methods
Method 1: Token Verification
Compare the AUTH_TOKEN
header with your stored webhook token:
require 'openssl' def verify_webhook(request) received_token = request.headers['AUTH_TOKEN'] expected_token = 'your_stored_webhook_token' # Use secure comparison to prevent timing attacks ActiveSupport::SecurityUtils.secure_compare(received_token, expected_token) end
Method 2: Company Verification
Verify the X-Fluid-Shop
header matches your expected company:
def verify_company(request) received_shop = request.headers['X-Fluid-Shop'] expected_shop = 'your-company-subdomain' received_shop == expected_shop end
Token Management
Retrieving Your Auth Token
Your webhook auth token is returned when you create a webhook:
curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resource": "order", "event": "created", "url": "https://your-app.com/webhooks/orders" }'
Response includes the auth_token
:
{ "webhook": { "id": 123, "auth_token": "abc123def456", ... } }
Custom Auth Tokens
You can specify a custom auth token when creating webhooks:
curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resource": "order", "event": "created", "url": "https://your-app.com/webhooks/orders", "auth_token": "your_custom_token_here" }'
Rotating Tokens
Update your webhook's auth token periodically for security:
curl -X PUT "https://api.fluid.app/api/company/webhooks/123" \ -H "Authorization: Bearer YOUR_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "auth_token": "new_secure_token_here" } }'
Security Best Practices
Endpoint Security
- Use HTTPS Only: Never use HTTP endpoints for webhooks
- Validate All Requests: Always verify the AUTH_TOKEN header
- Implement Rate Limiting: Protect against potential abuse
- Log Webhook Calls: Monitor for suspicious activity
Token Security
- Store Tokens Securely: Use environment variables or secure key management
- Rotate Regularly: Change tokens periodically
- Use Strong Tokens: Generate cryptographically secure random tokens
- Limit Token Scope: Use different tokens for different webhook types if possible
Example Secure Webhook Handler
class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token before_action :verify_webhook_auth def orders payload = JSON.parse(request.body.read) process_order_event(payload) head :ok rescue JSON::ParserError head :bad_request end private def verify_webhook_auth received_token = request.headers['AUTH_TOKEN'] expected_token = ENV['WEBHOOK_AUTH_TOKEN'] received_shop = request.headers['X-Fluid-Shop'] expected_shop = ENV['COMPANY_SUBDOMAIN'] unless received_token && expected_token head :unauthorized and return end unless received_shop == expected_shop head :unauthorized and return end unless ActiveSupport::SecurityUtils.secure_compare(received_token, expected_token) head :unauthorized and return end end def process_order_event(payload) event_name = payload.dig('payload', 'event_name') order_data = payload.dig('payload', 'order') case event_name when 'order_created' handle_new_order(order_data) when 'order_shipped' handle_order_shipped(order_data) # ... handle other events end end def handle_new_order(order_data) # Process new order Rails.logger.info "Processing new order: #{order_data['id']}" end def handle_order_shipped(order_data) # Process shipped order Rails.logger.info "Order shipped: #{order_data['id']}" end end
IP Allowlisting
For additional security, you may want to restrict webhook calls to specific IP addresses. Contact Fluid support for current webhook IP ranges if you need to implement IP allowlisting.
Best Practices
Implementation Patterns
Idempotency
Webhooks may be delivered more than once. Implement idempotency to handle duplicate events:
class WebhookProcessor def self.handle_webhook(payload) event_id = payload['id'] # Check if we've already processed this event return { status: 'already_processed' } if already_processed?(event_id) # Process the event result = process_event(payload) # Mark as processed mark_as_processed(event_id) result end private def self.already_processed?(event_id) ProcessedWebhook.exists?(event_id: event_id) end def self.mark_as_processed(event_id) ProcessedWebhook.create!(event_id: event_id, processed_at: Time.current) end def self.process_event(payload) # Event processing logic here { status: 'success' } end end
Asynchronous Processing
Process webhooks asynchronously to avoid timeouts:
class WebhooksController < ApplicationController def receive_webhook payload = JSON.parse(request.body.read) # Queue for background processing WebhookProcessorJob.perform_later(payload) head :ok end end class WebhookProcessorJob < ApplicationJob queue_as :webhooks def perform(payload) # Heavy processing here process_order_fulfillment(payload) end private def process_order_fulfillment(payload) event_name = payload.dig('payload', 'event_name') order_data = payload.dig('payload', 'order') case event_name when 'order_created' OrderFulfillmentService.new(order_data).process when 'order_shipped' ShippingNotificationService.new(order_data).send_notification end end end
Response Handling
Always return appropriate HTTP status codes:
class WebhooksController < ApplicationController def webhook_handler payload = JSON.parse(request.body.read) unless validate_payload(payload) head :bad_request and return end process_webhook(payload) head :ok rescue JSON::ParserError, ValidationError head :bad_request rescue ProcessingError head :unprocessable_entity rescue StandardError => e Rails.logger.error "Webhook processing error: #{e.message}" head :internal_server_error end private def validate_payload(payload) payload.present? && payload['payload'].present? end def process_webhook(payload) WebhookProcessor.handle_webhook(payload) end end
Error Handling and Retry Strategies
Webhook Delivery Behavior
- Fluid will retry failed webhook deliveries
- Retries use exponential backoff
- Maximum retry attempts: 5
- Timeout per request: 10 seconds
Handling Failures
Implement proper error handling in your webhook endpoints:
class WebhookProcessor def self.process_webhook(payload) # Main processing logic result = handle_event(payload) Rails.logger.info "Successfully processed webhook: #{payload['id']}" result rescue ValidationError => e Rails.logger.error "Validation error: #{e.message}" raise # Re-raise to return 400 status rescue ExternalServiceError => e Rails.logger.error "External service error: #{e.message}" # Don't re-raise - return success to prevent retries { status: 'deferred' } rescue StandardError => e Rails.logger.error "Unexpected error: #{e.message}" raise # Re-raise to trigger retry end private def self.handle_event(payload) event_name = payload.dig('payload', 'event_name') case event_name when 'order_created' OrderProcessor.new(payload.dig('payload', 'order')).process when 'cart_updated' CartProcessor.new(payload.dig('payload', 'cart')).process else Rails.logger.warn "Unknown event type: #{event_name}" end { status: 'success' } end end
Retry Logic
Implement your own retry logic for external service calls:
class ExternalServiceCaller MAX_RETRIES = 3 def self.call_with_retry(data) (0...MAX_RETRIES).each do |attempt| begin return external_service_call(data) rescue ExternalServiceError => e raise if attempt == MAX_RETRIES - 1 # Exponential backoff with jitter delay = (2 ** attempt) + rand sleep(delay) Rails.logger.warn "External service call failed (attempt #{attempt + 1}): #{e.message}" end end end private def self.external_service_call(data) # Make the actual external service call HTTParty.post('https://external-api.com/webhook', body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end end
Performance Considerations
Optimize Webhook Processing
- Keep Handlers Lightweight: Minimize processing time in webhook handlers
- Use Background Jobs: Queue heavy processing for background workers
- Database Optimization: Use efficient queries and indexing
- Caching: Cache frequently accessed data
Example Optimized Handler
class WebhooksController < ApplicationController def order_webhook payload = JSON.parse(request.body.read) # Quick validation and queuing unless quick_validate(payload) head :bad_request and return end # Queue for background processing using Sidekiq OrderWebhookJob.perform_async(payload) head :ok end private def quick_validate(payload) payload.present? && payload['payload'].present? && payload.dig('payload', 'order').present? end end # Background worker class OrderWebhookJob include Sidekiq::Job def perform(payload) process_order_webhook(payload) end private def process_order_webhook(payload) order_data = payload.dig('payload', 'order') event_name = payload.dig('payload', 'event_name') OrderWebhookProcessor.new(order_data, event_name).process end end
Monitoring and Alerting
Set up monitoring for your webhook endpoints:
class WebhooksController < ApplicationController around_action :monitor_webhook_performance def webhook_handler payload = JSON.parse(request.body.read) event_type = payload.dig('payload', 'event_name') || 'unknown' process_webhook(payload) # Log successful processing Rails.logger.info "Webhook processed successfully", event_type: event_type, webhook_id: payload['id'] # Increment success counter webhook_counter.increment(labels: { event_type: event_type, status: 'success' }) head :ok rescue StandardError => e # Log error Rails.logger.error "Webhook processing failed", event_type: event_type, error: e.message, webhook_id: payload&.dig('id') # Increment error counter webhook_counter.increment(labels: { event_type: event_type, status: 'error' }) head :internal_server_error end private def monitor_webhook_performance start_time = Time.current yield ensure duration = Time.current - start_time webhook_duration_histogram.observe(duration) end def webhook_counter @webhook_counter ||= Prometheus::Client.registry.counter( :webhook_requests_total, docstring: 'Total webhook requests', labels: [:event_type, :status] ) end def webhook_duration_histogram @webhook_duration_histogram ||= Prometheus::Client.registry.histogram( :webhook_processing_seconds, docstring: 'Webhook processing time' ) end end
Testing Webhooks
Local Development
Use tools like ngrok to expose local endpoints for testing:
# Install ngrok npm install -g ngrok # Expose local port 3000 ngrok http 3000 # Use the generated URL for webhook configuration # https://abc123.ngrok.io/webhooks/orders
Webhook Testing Tools
Create test endpoints to verify webhook delivery:
class WebhooksController < ApplicationController def test_webhook payload = JSON.parse(request.body.read) # Log the received payload Rails.logger.info "Received webhook: #{JSON.pretty_generate(payload)}" # Store for inspection in development if Rails.env.development? filename = "webhook_#{Time.current.to_i}.json" File.write(Rails.root.join('tmp', filename), JSON.pretty_generate(payload)) end head :ok rescue JSON::ParserError => e Rails.logger.error "Invalid JSON in webhook: #{e.message}" head :bad_request end end
Integration Testing
Test your webhook handlers with sample payloads:
require 'rails_helper' RSpec.describe WebhookProcessor do describe '.process_webhook' do let(:sample_payload) do { 'payload' => { 'event_name' => 'order_created', 'order' => { 'id' => 12345, 'order_number' => 'TEST-001', 'amount' => 99.99 } } } end it 'processes order created webhook successfully' do allow(OrderProcessor).to receive(:new).and_return(double(process: true)) result = described_class.process_webhook(sample_payload) expect(result[:status]).to eq('success') expect(OrderProcessor).to have_received(:new).with(sample_payload.dig('payload', 'order')) end it 'handles validation errors appropriately' do invalid_payload = { 'payload' => {} } expect { described_class.process_webhook(invalid_payload) } .to raise_error(ValidationError) end end end # Controller test RSpec.describe WebhooksController, type: :controller do describe 'POST #orders' do let(:valid_payload) { { payload: { event_name: 'order_created', order: { id: 1 } } } } before do request.headers['AUTH_TOKEN'] = 'valid_token' request.headers['X-Fluid-Shop'] = 'test-shop' allow(ENV).to receive(:[]).with('WEBHOOK_AUTH_TOKEN').and_return('valid_token') allow(ENV).to receive(:[]).with('COMPANY_SUBDOMAIN').and_return('test-shop') end it 'processes valid webhook and returns 200' do post :orders, 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 :orders, body: valid_payload.to_json expect(response).to have_http_status(:unauthorized) end end end
Need Help?
- API Documentation: Check our complete API reference
- Support: Contact our support team for webhook-specific questions
- Community: Join our developer community for tips and best practices
Ready to implement webhooks? Start by setting up your first webhook and testing it with our sample payloads!