Webhooks
Receive real-time notifications when events occur in your BotEsq account. Webhooks enable you to build responsive integrations without polling.
Overview
Webhooks are HTTP callbacks that notify your application when events occur. When an event happens (e.g., consultation completed), BotEsq sends an HTTP POST request to your configured endpoint with event details.
Setting Up Webhooks
- Log in to your operator dashboard
- Navigate to Settings > Webhooks
- Click "Add Endpoint"
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive
- Copy the signing secret for verification
Webhook Format
All webhooks are sent as HTTP POST requests with a JSON body:
{ "event": "consultation.completed", "timestamp": "2024-01-15T14:30:00Z", "webhook_id": "wh_abc123...", "data": { // Event-specific payload }}Headers
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-BotEsq-Signature | HMAC-SHA256 signature for verification |
| X-BotEsq-Timestamp | Unix timestamp when webhook was sent |
| X-BotEsq-Webhook-ID | Unique webhook delivery ID |
Signature Verification
Always verify webhook signatures to ensure requests are from BotEsq:
import { createHmac } from 'crypto'; function verifyWebhookSignature( payload: string, signature: string, timestamp: string, secret: string): boolean { // Check timestamp to prevent replay attacks const webhookTime = parseInt(timestamp); const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - webhookTime) > 300) { return false; // Reject webhooks older than 5 minutes } // Compute expected signature const signedPayload = `${timestamp}.${payload}`; const expectedSignature = createHmac('sha256', secret) .update(signedPayload) .digest('hex'); // Constant-time comparison to prevent timing attacks return signature === `sha256=${expectedSignature}`;} // Express.js exampleapp.post('/webhooks/botesq', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-botesq-signature']; const timestamp = req.headers['x-botesq-timestamp']; const payload = req.body.toString(); if (!verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(payload); // Handle the event... res.status(200).send('OK');});Sample Implementations
Complete webhook handler examples in popular languages. Each example includes signature verification and basic event handling.
import hmacimport hashlibimport timeimport jsonfrom flask import Flask, request, abort app = Flask(__name__)WEBHOOK_SECRET = "your_webhook_secret_here" def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool: """Verify the webhook signature from BotEsq.""" # Check timestamp to prevent replay attacks (5 minute window) webhook_time = int(timestamp) current_time = int(time.time()) if abs(current_time - webhook_time) > 300: return False # Compute expected signature signed_payload = f"{timestamp}.{payload.decode('utf-8')}" expected_signature = hmac.new( WEBHOOK_SECRET.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Constant-time comparison return hmac.compare_digest(signature, f"sha256={expected_signature}") @app.route('/webhooks/botesq', methods=['POST'])def handle_webhook(): # Get headers signature = request.headers.get('X-BotEsq-Signature', '') timestamp = request.headers.get('X-BotEsq-Timestamp', '') webhook_id = request.headers.get('X-BotEsq-Webhook-ID', '') # Verify signature if not verify_signature(request.data, signature, timestamp): abort(401, 'Invalid signature') # Parse event event = json.loads(request.data) event_type = event.get('event') # Handle different event types if event_type == 'consultation.completed': handle_consultation_completed(event['data']) elif event_type == 'document.analyzed': handle_document_analyzed(event['data']) elif event_type == 'matter.status_changed': handle_matter_status_changed(event['data']) elif event_type == 'credits.low': handle_credits_low(event['data']) elif event_type == 'retainer.expiring': handle_retainer_expiring(event['data']) else: print(f"Unhandled event type: {event_type}") return 'OK', 200 def handle_consultation_completed(data: dict): print(f"Consultation {data['consultation_id']} completed") # Add your business logic here def handle_document_analyzed(data: dict): print(f"Document {data['document_id']} analyzed ({data['page_count']} pages)") # Add your business logic here def handle_matter_status_changed(data: dict): print(f"Matter {data['matter_id']}: {data['previous_status']} -> {data['new_status']}") # Add your business logic here def handle_credits_low(data: dict): print(f"Low credits warning: {data['credits_remaining']} remaining") # Add your business logic here def handle_retainer_expiring(data: dict): print(f"Retainer {data['retainer_id']} expiring at {data['expires_at']}") # Add your business logic here if __name__ == '__main__': app.run(port=3000)Available Events
consultation.completed
A consultation has been completed by an attorney
Example Payload
{ "event": "consultation.completed", "timestamp": "2024-01-15T14:30:00Z", "data": { "consultation_id": "con_abc123...", "matter_id": "mat_xyz789...", "status": "completed", "attorney_id": "atty_def456...", "completed_at": "2024-01-15T14:30:00Z" }}document.analyzed
Document analysis has been completed
Example Payload
{ "event": "document.analyzed", "timestamp": "2024-01-15T12:00:00Z", "data": { "document_id": "doc_ghi789...", "matter_id": "mat_xyz789...", "status": "completed", "page_count": 15, "attorney_id": "atty_def456..." }}matter.status_changed
A matter status has changed
Example Payload
{ "event": "matter.status_changed", "timestamp": "2024-01-15T10:00:00Z", "data": { "matter_id": "mat_xyz789...", "previous_status": "pending_retainer", "new_status": "active" }}credits.low
Account credits have fallen below threshold
Example Payload
{ "event": "credits.low", "timestamp": "2024-01-15T09:00:00Z", "data": { "operator_id": "op_abc123...", "credits_remaining": 5000, "threshold": 10000 }}retainer.expiring
A retainer offer is about to expire
Example Payload
{ "event": "retainer.expiring", "timestamp": "2024-01-15T08:00:00Z", "data": { "retainer_id": "ret_abc123...", "matter_id": "mat_xyz789...", "expires_at": "2024-01-16T08:00:00Z" }}Best Practices
- Respond quickly (under 5 seconds) - Process webhooks asynchronously if needed
- Return 2xx status - Any other status triggers retries
- Handle duplicates - Use webhook_id for idempotency
- Verify signatures - Always validate the X-BotEsq-Signature header
- Log webhook_id - For debugging and support requests
Retry Policy
If your endpoint returns a non-2xx status or times out, BotEsq retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry (final) | 24 hours |
After 5 failed attempts, the webhook is marked as failed and will not be retried. You can manually retry failed webhooks from the dashboard.