# Webhooks Receive real-time notifications when quote events occur in Powerlily. Webhooks push data to your server automatically, eliminating the need to poll the API and enabling instant integrations with your CRM, project management tools, and custom workflows. ## What are Webhooks? Webhooks are HTTP callbacks that send data to your server when specific events occur. Unlike the API where you request data, webhooks actively push data to your endpoint when something happens. **When to Use Webhooks vs. API:** - **Webhooks** - Real-time notifications when quotes are created, updated, or signed - **API** - Fetch historical data, poll for changes, or query specific records - **Both** - Use webhooks for notifications, then fetch full details via API **Common Use Cases:** - **CRM Sync** - Automatically create deals when quotes are signed - **Team Notifications** - Send Slack/email alerts for new quotes - **Project Management** - Create tasks when quotes are won - **Analytics** - Track quote metrics in real-time - **Custom Workflows** - Trigger automation in your own systems --- ## Setting Up Webhooks ### 1. Configure Your Endpoint Create a public HTTPS endpoint that can receive POST requests from Powerlily. **Requirements:** - Must use HTTPS (not HTTP) - Must be publicly accessible - Should respond with 2xx status code within 10 seconds - Should handle duplicate deliveries (idempotent) **Example Endpoint:** ``` https://yourserver.com/webhooks/powerlily ``` ### 2. Enable Webhooks in Powerlily 1. Log into Powerlily 2. Go to **Company Settings** > **API Settings** 3. Scroll to the **Webhooks** section 4. Enter your webhook URL 5. Check **Enable webhooks** 6. Click **Save Webhook Settings** **Note:** A signing secret is automatically generated when you enable webhooks. ### 3. Test Your Integration Click **Send Test Webhook** to verify your endpoint is working correctly. Check the **Recent Deliveries** section to see if the delivery was successful. --- ## Webhook Events Powerlily sends webhooks for the following quote events: ### quote.created Triggered when a new quote is created. **Use Case:** Notify your team, create a CRM lead, or trigger follow-up workflows. ### quote.updated Triggered when a quote is modified (excluding when it's signed). **Use Case:** Sync changes to your CRM, track quote iterations, or update external systems. ### quote.signed Triggered when a customer signs a quote. **Use Case:** Create won deals, notify sales team, initiate project setup, or trigger celebration workflows. **Note:** When a quote is signed, only `quote.signed` is sent (not both `quote.updated` and `quote.signed`). ### test Triggered manually when you click "Send Test Webhook" in settings. **Use Case:** Verify your endpoint is configured correctly and receiving webhooks. --- ## Webhook Payload Structure All webhooks follow this JSON structure: ```json { "event": "quote.created", "timestamp": "2025-01-15T14:30:00Z", "data": { // Event-specific data (see below) } } ``` ### Payload Fields | Field | Type | Description | |---|---|---| | `event` | string | Event type (e.g., `quote.created`, `quote.signed`) | | `timestamp` | string | ISO 8601 timestamp when webhook was sent | | `data` | object | Event data (quote details) | --- ## Quote Data Structure The `data` object contains comprehensive quote information: ### Basic Information ```json { "id": "14bed4b2-0bf6-41ac-8f86-77eb330fe63a", "slug": "john-smith-abc123", "client_name": "John Smith", "client_email": "[email protected]", "phone": "902-555-1234", "address": "123 Solar Lane, Halifax, NS", "status": "quote" } ``` | Field | Type | Description | |---|---|---| | `id` | string | Unique quote UUID | | `slug` | string | URL-friendly identifier | | `client_name` | string | Customer name | | `client_email` | string | Customer email | | `phone` | string | Customer phone | | `address` | string | Installation address | | `status` | string | Quote status (e.g., `quote`, `won`, `lost`) | ### Option/Variant Information ```json { "option_title": "Standard Package", "option_description": "8.4 kW system with battery storage" } ``` | Field | Type | Description | |---|---|---| | `option_title` | string | Quote option/variant name | | `option_description` | string | Option description | ### Pricing Information ```json { "pricing_model": "bom", "price_per_watt": null, "total_hardware_cost": 18000.00, "installation_cost": 5000.00, "gross_cost": 28750.00, "net_cost": 25000.00, "total_incentive_value": 3000.00 } ``` | Field | Type | Description | |---|---|---| | `pricing_model` | string | Pricing model (`bom` or `ppw`) | | `price_per_watt` | float | Price per watt (if using PPW model) | | `total_hardware_cost` | float | Total equipment cost | | `installation_cost` | float | Labor and installation cost | | `gross_cost` | float | Total cost including taxes | | `net_cost` | float | Total cost after incentives | | `total_incentive_value` | float | Total value of incentives | ### Production & Energy ```json { "total_production_kwh": 9850.50, "yearly_consumption_kwh": 9600.00, "offset_percentage": 102.6, "monthly_bill": 175.00 } ``` | Field | Type | Description | |---|---|---| | `total_production_kwh` | float | Annual AC production (kWh) | | `yearly_consumption_kwh` | float | Customer's annual consumption | | `offset_percentage` | float | Percentage of usage offset by solar | | `monthly_bill` | float | Customer's current monthly bill | ### Status Fields ```json { "signed": true, "signed_at": "2025-01-20T10:30:00Z", "sent": true, "times_opened": 5, "customer_notes": "Prefer installation in spring" } ``` | Field | Type | Description | |---|---|---| | `signed` | boolean | Whether quote is signed | | `signed_at` | string | ISO 8601 timestamp when signed | | `sent` | boolean | Whether quote has been sent to customer | | `times_opened` | integer | Number of times customer viewed quote | | `customer_notes` | string | Notes from customer | ### Stage & Workflow ```json { "stage_name": "Proposal Sent", "workflow_name": "Sales Pipeline" } ``` | Field | Type | Description | |---|---|---| | `stage_name` | string | Current workflow stage | | `workflow_name` | string | Name of the workflow/pipeline | ### Equipment Arrays #### Panels ```json { "panels": [ { "name": "Canadian Solar 420W", "quantity": 20, "per_unit_value": 300.00, "total_value": 6000.00 } ] } ``` #### Inverters ```json { "inverters": [ { "name": "SolarEdge SE7600H", "quantity": 1, "per_unit_value": 2500.00, "total_value": 2500.00 } ] } ``` #### Additional Equipment ```json { "equipment": [ { "name": "Tesla Powerwall 3", "quantity": 1, "per_unit_value": 8500.00, "total_value": 8500.00 } ] } ``` ### PV Arrays ```json { "pv_arrays": [ { "id": 123, "array_number": 1, "panel_name": "Canadian Solar 420W", "inverter_name": "SolarEdge SE7600H", "system_capacity_kw": 8.4, "ac_annual_kwh": 9850.50, "azimuth": 180, "tilt": 25, "number_of_panels": 20, "number_of_inverters": 1 } ] } ``` | Field | Type | Description | |---|---|---| | `id` | integer | Array ID | | `array_number` | integer | Array sequence number | | `panel_name` | string | Panel model name | | `inverter_name` | string | Inverter model name | | `system_capacity_kw` | float | DC capacity in kilowatts | | `ac_annual_kwh` | float | Annual AC production | | `azimuth` | integer | Compass direction (180 = south) | | `tilt` | integer | Roof pitch in degrees | | `number_of_panels` | integer | Panels in this array | | `number_of_inverters` | integer | Inverters in this array | ### Timestamps ```json { "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-20T14:30:00Z" } ``` --- ## Complete Example Payloads ### quote.created ```json { "event": "quote.created", "timestamp": "2025-01-15T10:00:00Z", "data": { "id": "14bed4b2-0bf6-41ac-8f86-77eb330fe63a", "slug": "john-smith-abc123", "client_name": "John Smith", "client_email": "[email protected]", "phone": "902-555-1234", "address": "123 Solar Lane, Halifax, NS", "status": "quote", "option_title": "Standard Package", "option_description": "8.4 kW residential solar", "pricing_model": "bom", "price_per_watt": null, "total_hardware_cost": 18000.00, "installation_cost": 5000.00, "gross_cost": 28750.00, "net_cost": 25000.00, "total_incentive_value": 3000.00, "total_production_kwh": 9850.50, "yearly_consumption_kwh": 9600.00, "offset_percentage": 102.6, "monthly_bill": 175.00, "signed": false, "signed_at": null, "sent": false, "times_opened": 0, "customer_notes": null, "stage_name": "Design Complete", "workflow_name": "Sales Pipeline", "panels": [ { "name": "Canadian Solar 420W", "quantity": 20, "per_unit_value": 300.00, "total_value": 6000.00 } ], "inverters": [ { "name": "SolarEdge SE7600H", "quantity": 1, "per_unit_value": 2500.00, "total_value": 2500.00 } ], "equipment": [ { "name": "Tesla Powerwall 3", "quantity": 1, "per_unit_value": 8500.00, "total_value": 8500.00 } ], "pv_arrays": [ { "id": 123, "array_number": 1, "panel_name": "Canadian Solar 420W", "inverter_name": "SolarEdge SE7600H", "system_capacity_kw": 8.4, "ac_annual_kwh": 9850.50, "azimuth": 180, "tilt": 25, "number_of_panels": 20, "number_of_inverters": 1 } ], "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:00:00Z" } } ``` ### quote.signed ```json { "event": "quote.signed", "timestamp": "2025-01-20T14:30:00Z", "data": { "id": "14bed4b2-0bf6-41ac-8f86-77eb330fe63a", "slug": "john-smith-abc123", "client_name": "John Smith", "client_email": "[email protected]", "phone": "902-555-1234", "address": "123 Solar Lane, Halifax, NS", "status": "won", "signed": true, "signed_at": "2025-01-20T14:30:00Z", "sent": true, "times_opened": 5, "stage_name": "Contract Signed", "workflow_name": "Sales Pipeline", "net_cost": 25000.00, "total_production_kwh": 9850.50, // ... (same structure as quote.created) } } ``` ### test ```json { "event": "test", "timestamp": "2025-01-15T15:45:00Z", "data": { "id": "test-webhook-a3f8bc21", "message": "This is a test webhook from Powerlily", "timestamp": "2025-01-15T15:45:00Z" } } ``` --- ## Security: Verifying Webhooks Powerlily signs all webhook requests with an HMAC signature to prove authenticity. Always verify signatures to prevent malicious requests. ### How Signing Works 1. Powerlily generates a secret when you enable webhooks 2. For each webhook, Powerlily creates an HMAC SHA256 signature of the payload 3. The signature is sent in the `X-Webhook-Signature` header 4. Your server recomputes the signature and compares it ### Implementation Examples #### Python (Flask) ```python import hmac import hashlib from flask import request, abort WEBHOOK_SECRET = 'your_webhook_secret_from_powerlily' @app.route('/webhooks/powerlily', methods=['POST']) def handle_webhook(): # Get signature from header signature = request.headers.get('X-Webhook-Signature') if not signature: abort(401) # Get raw payload payload = request.get_data() # Compute expected signature expected = hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() # Compare signatures (timing-safe) if not hmac.compare_digest(signature, expected): abort(401) # Signature verified! Process webhook data = request.get_json() event = data['event'] if event == 'quote.signed': handle_quote_signed(data['data']) elif event == 'quote.created': handle_quote_created(data['data']) return '', 200 ``` #### Node.js (Express) ```javascript const express = require('express'); const crypto = require('crypto'); const WEBHOOK_SECRET = 'your_webhook_secret_from_powerlily'; app.post('/webhooks/powerlily', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; if (!signature) { return res.status(401).send('Missing signature'); } // Compute expected signature const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.body) .digest('hex'); // Compare signatures if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } // Signature verified! Process webhook const payload = JSON.parse(req.body); const { event, data } = payload; if (event === 'quote.signed') { handleQuoteSigned(data); } else if (event === 'quote.created') { handleQuoteCreated(data); } res.status(200).send('OK'); }); ``` #### PHP ```php <?php $webhookSecret = 'your_webhook_secret_from_powerlily'; // Get signature from header $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; if (empty($signature)) { http_response_code(401); exit('Missing signature'); } // Get raw payload $payload = file_get_contents('php://input'); // Compute expected signature $expected = hash_hmac('sha256', $payload, $webhookSecret); // Compare signatures if (!hash_equals($signature, $expected)) { http_response_code(401); exit('Invalid signature'); } // Signature verified! Process webhook $data = json_decode($payload, true); $event = $data['event']; if ($event === 'quote.signed') { handleQuoteSigned($data['data']); } elseif ($event === 'quote.created') { handleQuoteCreated($data['data']); } http_response_code(200); echo 'OK'; ``` #### Ruby (Rails) ```ruby class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token WEBHOOK_SECRET = ENV['POWERLILY_WEBHOOK_SECRET'] def powerlily signature = request.headers['X-Webhook-Signature'] unless signature.present? head :unauthorized return end # Get raw payload payload = request.raw_post # Compute expected signature expected = OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, payload) # Compare signatures unless ActiveSupport::SecurityUtils.secure_compare(signature, expected) head :unauthorized return end # Signature verified! Process webhook data = JSON.parse(payload) event = data['event'] case event when 'quote.signed' handle_quote_signed(data['data']) when 'quote.created' handle_quote_created(data['data']) end head :ok end private def handle_quote_signed(quote) # Your logic here end def handle_quote_created(quote) # Your logic here end end ``` --- ## Best Practices ### 1. Respond Quickly Return a `200 OK` response within 10 seconds, even if you haven't finished processing. Process webhooks asynchronously using background jobs. **Good:** ```python @app.route('/webhooks/powerlily', methods=['POST']) def handle_webhook(): # Verify signature verify_signature(request) # Queue for background processing process_webhook_job.delay(request.get_json()) return '', 200 # Respond immediately ``` **Bad:** ```python @app.route('/webhooks/powerlily', methods=['POST']) def handle_webhook(): verify_signature(request) # This might take too long! sync_to_crm(request.get_json()) send_notifications() update_analytics() return '', 200 # Response delayed ``` ### 2. Handle Idempotency Webhooks may be delivered more than once. Use the quote `id` to prevent duplicate processing. **Example:** ```python def handle_quote_signed(quote_data): quote_id = quote_data['id'] # Check if already processed if redis.exists(f'processed:{quote_id}'): return # Skip duplicate # Process webhook create_crm_deal(quote_data) # Mark as processed redis.set(f'processed:{quote_id}', '1', ex=86400) # 24 hour TTL ``` ### 3. Use the API for Full Details Webhooks contain comprehensive data, but if you need additional details or want to ensure you have the latest state, fetch from the API: ```python def handle_quote_signed(quote_data): quote_id = quote_data['id'] # Get full details via API response = requests.get( f'https://yourcompany.powerlily.io/api/v1/quotes/{quote_id}', headers={'X-Api-Key': API_KEY} ) full_quote = response.json()['data'] create_crm_deal(full_quote) ``` ### 4. Log Everything Keep detailed logs of webhook deliveries for debugging: ```python def handle_webhook(): payload = request.get_json() # Log receipt logger.info(f"Received webhook: {payload['event']}", extra={ 'event': payload['event'], 'quote_id': payload['data'].get('id'), 'timestamp': payload['timestamp'] }) try: process_webhook(payload) logger.info("Webhook processed successfully") except Exception as e: logger.error(f"Webhook processing failed: {e}", exc_info=True) ``` ### 5. Return Proper Status Codes | Status | When to Use | |---|---| | **200 OK** | Webhook received and verified | | **401 Unauthorized** | Invalid or missing signature | | **400 Bad Request** | Malformed payload | | **500 Server Error** | Internal error (Powerlily will retry) | ### 6. Implement Exponential Backoff If your server is temporarily unavailable, return a `5xx` error so Powerlily retries delivery. **Note:** Powerlily does not automatically retry failed webhooks in the current implementation. Build your own monitoring to detect failed deliveries. ### 7. Secure Your Endpoint - Always use HTTPS - Verify webhook signatures on every request - Don't expose sensitive data in responses - Rate limit webhook requests - Monitor for suspicious activity ### 8. Monitor Delivery Status Check the **Recent Deliveries** section in API Settings regularly: - **Success (200-299)** - Webhook delivered successfully - **Failed (400-599)** - Server returned error - **Error** - Network error or timeout --- ## Common Integration Patterns ### Pattern 1: Signed Quote → Salesforce Opportunity ```python def handle_quote_signed(quote_data): salesforce_client = Salesforce( username=SF_USERNAME, password=SF_PASSWORD, security_token=SF_TOKEN ) # Create opportunity opportunity = { 'Name': f"{quote_data['client_name']} - Solar Installation", 'StageName': 'Proposal Accepted', 'Amount': quote_data['net_cost'], 'CloseDate': (datetime.now() + timedelta(days=30)).isoformat(), 'Description': f""" System: {quote_data['total_production_kwh']} kWh/year Address: {quote_data['address']} Email: {quote_data['client_email']} Phone: {quote_data['phone']} """ } salesforce_client.Opportunity.create(opportunity) ``` ### Pattern 2: New Quote → Slack Notification ```python def handle_quote_created(quote_data): slack_webhook_url = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL' message = { 'text': '📄 New Quote Created!', 'blocks': [ { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f"*{quote_data['client_name']}*\n{quote_data['address']}" } }, { 'type': 'section', 'fields': [ {'type': 'mrkdwn', 'text': f"*System:*\n{quote_data['total_production_kwh']:.0f} kWh/year"}, {'type': 'mrkdwn', 'text': f"*Price:*\n${quote_data['net_cost']:,.2f}"}, {'type': 'mrkdwn', 'text': f"*Email:*\n{quote_data['client_email']}"}, {'type': 'mrkdwn', 'text': f"*Phone:*\n{quote_data['phone']}"} ] } ] } requests.post(slack_webhook_url, json=message) ``` ### Pattern 3: Quote Updated → Update Google Sheets ```python from googleapiclient.discovery import build def handle_quote_updated(quote_data): service = build('sheets', 'v4', credentials=credentials) spreadsheet_id = 'YOUR_SPREADSHEET_ID' # Find row by quote ID range_name = 'Quotes!A:A' result = service.spreadsheets().values().get( spreadsheetId=spreadsheet_id, range=range_name ).execute() values = result.get('values', []) row_index = None for i, row in enumerate(values): if row and row[0] == quote_data['id']: row_index = i + 1 break if row_index: # Update existing row range_name = f'Quotes!A{row_index}:J{row_index}' values = [[ quote_data['id'], quote_data['client_name'], quote_data['client_email'], quote_data['address'], quote_data['net_cost'], quote_data['status'], quote_data['stage_name'], 'Yes' if quote_data['signed'] else 'No', quote_data['times_opened'], quote_data['updated_at'] ]] service.spreadsheets().values().update( spreadsheetId=spreadsheet_id, range=range_name, valueInputOption='RAW', body={'values': values} ).execute() ``` ### Pattern 4: Signed Quote → Email Team ```python from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail def handle_quote_signed(quote_data): message = Mail( from_email='[email protected]', to_emails=['[email protected]', '[email protected]'], subject=f'🎉 Quote Signed: {quote_data["client_name"]}', html_content=f""" <h2>Quote Signed!</h2> <p><strong>{quote_data['client_name']}</strong> just signed their quote.</p> <h3>Project Details</h3> <ul> <li><strong>System Size:</strong> {quote_data['total_production_kwh']:.0f} kWh/year</li> <li><strong>Total Cost:</strong> ${quote_data['net_cost']:,.2f}</li> <li><strong>Address:</strong> {quote_data['address']}</li> <li><strong>Contact:</strong> {quote_data['client_email']} / {quote_data['phone']}</li> </ul> <p><a href="https://yourcompany.powerlily.io/quotes/{quote_data['id']}">View in Powerlily</a></p> """ ) sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) sg.send(message) ``` --- ## Troubleshooting ### Webhooks Not Being Delivered **Check:** - Is "Enable webhooks" checked in settings? - Is your webhook URL valid and publicly accessible? - Is your endpoint using HTTPS (not HTTP)? - Does your server respond within 10 seconds? - Check **Recent Deliveries** for error details ### 401 Unauthorized in Recent Deliveries **Cause:** Your endpoint is rejecting the webhook (likely signature verification failed) **Solutions:** - Verify you're using the correct signing secret - Check your signature verification code - Ensure you're comparing signatures in a timing-safe manner - Test with a simple endpoint that always returns 200 first ### Timeouts **Cause:** Your endpoint takes too long to respond (>10 seconds) **Solutions:** - Respond immediately with 200, process asynchronously - Use background jobs (Sidekiq, Celery, etc.) - Optimize slow database queries - Reduce external API calls during webhook processing ### Duplicate Webhooks **Cause:** Webhooks may be delivered multiple times **Solutions:** - Implement idempotency using quote ID - Cache processed webhook IDs in Redis - Use database unique constraints - Check timestamps before processing ### Missing Fields **Cause:** Some fields may be null if not set **Solutions:** - Always check for null/None before accessing nested data - Use safe navigation operators (`?.` in Ruby, `?.` in JavaScript) - Provide default values: `quote_data.get('phone') or 'N/A'` --- ## Testing Your Integration ### 1. Use the Test Webhook Button Click **Send Test Webhook** in API Settings to verify basic connectivity. ### 2. Create a Test Quote Create and update a test quote to trigger real webhooks: 1. Create a new quote 2. Check your endpoint received `quote.created` 3. Update the quote 4. Check for `quote.updated` 5. Mark it as signed 6. Check for `quote.signed` ### 3. Test with RequestBin Before building your endpoint, test with a service like [RequestBin](https://requestbin.com): 1. Create a RequestBin URL 2. Enter it as your webhook URL in Powerlily 3. Send a test webhook 4. View the raw payload in RequestBin 5. Use this to build your actual endpoint ### 4. Local Testing with ngrok Test webhooks on localhost using [ngrok](https://ngrok.com): ```bash # Start your local server python app.py # Running on localhost:5000 # In another terminal, start ngrok ngrok http 5000 # Use the ngrok HTTPS URL as your webhook URL # Example: https://abc123.ngrok.io/webhooks/powerlily ``` ### 5. Verify Signature Implementation Test your signature verification: ```python # Test with known values payload = '{"event":"test","data":{"message":"test"}}' secret = 'your_secret' expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest() print(f"Payload: {payload}") print(f"Secret: {secret}") print(f"Signature: {expected}") # Now send this to your endpoint with the signature header ``` --- ## Monitoring & Debugging ### Check Recent Deliveries The **Recent Deliveries** section shows the last 10 webhook attempts: - **Status** - Success, Failed, or Error - **Event** - The event type - **Time** - When it was sent - **Code** - HTTP response code (if any) ### Read Response Codes | Code | Meaning | Action | |---|---|---| | 200-299 | Success | Webhook delivered | | 401 | Unauthorized | Check signature verification | | 404 | Not Found | Verify webhook URL is correct | | 500 | Server Error | Check your endpoint logs | | Timeout | No response in 10s | Respond faster | ### Debugging Tips **Log the raw payload:** ```python @app.route('/webhooks/powerlily', methods=['POST']) def handle_webhook(): # Log everything for debugging print(f"Headers: {dict(request.headers)}") print(f"Body: {request.get_data()}") # Then process normally verify_signature(request) process_webhook(request.get_json()) return '', 200 ``` **Temporarily disable signature verification:** ```python # ONLY for debugging! Re-enable after testing @app.route('/webhooks/powerlily', methods=['POST']) def handle_webhook(): # Skip verification temporarily # verify_signature(request) data = request.get_json() print(f"Received: {data}") return '', 200 ``` **Check webhook delivery details:** Look at the full error message in Powerlily's Recent Deliveries section. Click on a delivery to see response body (if available). --- ## Rate Limits Webhook deliveries are subject to the same rate limits as API requests: - **100 webhooks per minute** per company - If you exceed this, new webhooks will be queued and delivered when the limit resets **Best Practices:** - Process webhooks quickly to avoid timeouts - Don't make excessive external API calls during webhook processing - Use background jobs for heavy processing --- ## Regenerating Your Signing Secret If your signing secret is compromised: 1. Go to **API Settings** > **Webhooks** 2. Click **Regenerate Secret** 3. Copy the new secret 4. Update your server code with the new secret 5. Test your integration **Important:** Your old secret stops working immediately. Update your code before regenerating. --- ## Related Documentation - [[settings/API Settings|API Settings]] - Complete API reference for fetching additional data - [[settings/Zapier Integration|Zapier Integration]] - Alternative integration method using polling - [[settings/Domain Settings|Domain Settings]] - Configure your webhook base URL - [[CRM/Workflows|Workflows]] - Understand workflow stages in webhook payloads --- ## Support Need help with webhooks? **Resources:** - Review webhook delivery logs in API Settings - Test with RequestBin or ngrok first - Check signature verification implementation - Review code examples above **Contact:** - Email: [email protected] - Subject: Include "Webhook Support" - Include: Webhook delivery logs, error messages, and code snippets --- *Last updated: January 2026*