Webhooks Guide
Last Updated: 2026-01-10 Status: Implemented Plan Reference: 023-webhooks-event-integration.md, 059-webhooks-enhancement.md
Overview
The Webhooks system enables external integrations by sending HTTP callbacks when events occur in the portal. Third-party systems can subscribe to events like invoice creation, project updates, and file uploads to automate workflows and keep systems in sync.
Table of Contents
- Accessing Webhooks
- Features
- How to Use
- Event Types
- Payload Format
- Signature Verification
- Interactive Testing
- Delivery Monitoring
- Technical Architecture
- Related Features
Accessing Webhooks
Navigation
| Access Point | Location | URL | Role |
|---|---|---|---|
| Webhook Management | Admin sidebar | /admin/webhooks | Admin |
| Create Webhook | Webhooks page | /admin/webhooks/create | Admin |
| Webhook Details | Webhook list | /admin/webhooks/{id} | Admin |
| Test Interface | Webhook detail | /admin/webhooks/{id}/test-form | Admin |
| Delivery Logs | Webhook detail | /admin/webhooks/{id}/deliveries | Admin |
| All Deliveries | Webhooks menu | /admin/webhooks-all-deliveries | Admin |
| Documentation | Webhooks menu | /admin/webhooks-documentation | Admin |
Permissions
| Action | Admin | Client User |
|---|---|---|
| View webhooks | ✅ | ❌ |
| Create webhooks | ✅ | ❌ |
| Edit webhooks | ✅ | ❌ |
| Delete webhooks | ✅ | ❌ |
| View logs | ✅ | ❌ |
| Test webhooks | ✅ | ❌ |
| View documentation | ✅ | ❌ |
Features
Webhook Management
- Create multiple webhooks per client or global
- Subscribe to specific events
- Custom headers support
- Enable/disable toggle
- Auto-generated or custom secret key
- Regenerate secret on demand
Configuration Options
- Timeout: Configurable timeout per endpoint (default 30s)
- Max Retries: Configurable retry attempts (default 5)
- Custom Headers: Add custom headers to requests
- Payload Logging: Toggle payload logging for debugging
Interactive Testing (New)
- Visual testing interface
- Select from all event types
- Edit payloads with JSON validation
- View real-time results
- Inspect request/response headers
- Verify signature generation
Delivery Monitoring (Enhanced)
- Delivery attempt history
- Request/response headers logging
- Error message tracking
- Duration metrics
- Global delivery dashboard
- Filter by endpoint, status, event, date
Security
- HMAC-SHA256 signature verification
- Timestamp validation (replay attack prevention)
- Secret key per webhook
- HTTPS enforcement (production)
- Request timeout limits
How to Use
Creating a Webhook
- Navigate to Admin → Webhooks
- Click "Create Webhook"
- Fill in details:
- Name: Descriptive name
- URL: Endpoint to receive callbacks
- Client: Optional - associate with specific client
- Events: Select events to subscribe
- Configure advanced options:
- Timeout: Request timeout (1-60 seconds)
- Max Retries: Retry attempts (1-10)
- Custom Headers: Add key-value pairs
- Click "Create Webhook"
- Save the secret key - shown only once
Testing a Webhook (Interactive)
- Navigate to webhook detail page
- Click "Test" button
- In the testing interface:
- Select event type from dropdown
- View/edit the sample payload
- Click "Send Test Webhook"
- Review results:
- Success/failure status
- HTTP status code
- Response duration
- Signature used
- Request/response headers
- Response body
Viewing Delivery Logs
- Navigate to webhook detail page
- Click "Deliveries" tab
- View delivery attempts:
- Delivery ID
- Event type
- Status (pending/success/failed)
- Response code
- Duration
- Attempt count
- Click a delivery for detailed view:
- Full request/response headers
- Payload JSON
- Individual attempt history
- Error messages
Global Delivery Monitoring
- Navigate to Admin → Webhooks → All Deliveries
- View aggregated statistics:
- Total deliveries
- Success/failure counts
- Success rate percentage
- Average duration
- Failed in last 24h
- Filter by:
- Endpoint
- Status
- Event type
- Date range
Retrying Failed Deliveries
- Navigate to delivery detail or all deliveries
- Click "Retry" button on failed delivery
- Delivery is re-queued with reset attempt count
Event Types
Client Events
| Event | Trigger | Payload |
|---|---|---|
client.created | New client created | Client data |
client.updated | Client info updated | Client data |
client.deleted | Client deleted | Client ID |
Project Events
| Event | Trigger | Payload |
|---|---|---|
project.created | New project created | Project data |
project.updated | Project updated | Project data |
project.status_changed | Status changed | Project + old/new status |
project.deleted | Project deleted | Project ID |
Invoice Events
| Event | Trigger | Payload |
|---|---|---|
invoice.created | Invoice created | Invoice data |
invoice.updated | Invoice updated | Invoice data |
invoice.sent | Invoice marked sent | Invoice + sent_to |
invoice.paid | Invoice marked paid | Invoice + payment info |
invoice.overdue | Invoice became overdue | Invoice + days overdue |
invoice.deleted | Invoice deleted | Invoice ID |
File Events
| Event | Trigger | Payload |
|---|---|---|
file.uploaded | File uploaded | File metadata |
file.deleted | File deleted | File ID |
User Events
| Event | Trigger | Payload |
|---|---|---|
user.created | New user created | User data |
user.updated | User updated | User data |
System Events
| Event | Trigger | Payload |
|---|---|---|
webhook.test | Test webhook sent | Test message |
Payload Format
Standard Payload Structure
{
"id": "test_abc123def456",
"event": "invoice.created",
"created_at": "2026-01-10T14:30:00+00:00",
"test": false,
"timestamp": "2026-01-10T14:30:00+00:00",
"data": {
"id": 12345,
"number": "INV-2026-0001",
"client_id": 1,
"total": 1650.00,
"currency": "USD",
"status": "draft"
}
}
Request Headers
Content-Type: application/json
Accept: application/json
X-Webhook-Signature: t=1704898200,v1=abc123...
X-Webhook-Event: invoice.created
X-Webhook-Endpoint-Id: 1
X-Webhook-Test: false
User-Agent: ClientPortal-Webhook/1.0
Signature Verification
Signature Format
Each request includes an X-Webhook-Signature header:
t=1704898200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Where:
- t: Unix timestamp when signature was created
- v1: HMAC-SHA256 signature
Signed Payload
The signature is computed over:
{timestamp}.{json_payload}
Verification Steps
- Parse the signature header to extract
tandv1 - Check timestamp is within 5 minutes (prevents replay attacks)
- Compute expected signature:
HMAC-SHA256(secret, "{t}.{payload}") - Compare using constant-time comparison
PHP Example
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$secret = 'your_webhook_secret';
// Parse signature
preg_match('/t=(\d+),v1=(.+)/', $signature, $matches);
$timestamp = $matches[1];
$providedSignature = $matches[2];
// Verify timestamp (within 5 minutes)
if (abs(time() - $timestamp) > 300) {
http_response_code(401);
exit('Signature expired');
}
// Verify signature
$expectedSignature = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);
if (!hash_equals($expectedSignature, $providedSignature)) {
http_response_code(401);
exit('Invalid signature');
}
// Process webhook...
$data = json_decode($payload, true);
Node.js Example
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const match = signature.match(/t=(\d+),v1=(.+)/);
if (!match) return false;
const timestamp = parseInt(match[1]);
const providedSignature = match[2];
// Check timestamp (within 5 minutes)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
// Express middleware example
app.post('/webhook', (req, res) => {
const payload = JSON.stringify(req.body);
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
res.status(200).send('OK');
});
Python Example
import hmac
import hashlib
import time
import re
def verify_webhook(payload: str, signature: str, secret: str) -> bool:
match = re.match(r't=(\d+),v1=(.+)', signature)
if not match:
return False
timestamp = int(match.group(1))
provided_signature = match.group(2)
# Check timestamp (within 5 minutes)
if abs(time.time() - timestamp) > 300:
return False
# Verify signature
expected_signature = hmac.new(
secret.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, provided_signature)
# Flask example
@app.route('/webhook', methods=['POST'])
def handle_webhook():
payload = request.get_data(as_text=True)
signature = request.headers.get('X-Webhook-Signature')
if not verify_webhook(payload, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
# Process webhook...
data = request.get_json()
return 'OK', 200
Interactive Testing
The interactive testing interface allows you to send test webhooks with custom payloads.
Features
- Event Selection: Choose from all available event types
- Sample Payloads: Pre-populated with realistic sample data
- JSON Editing: Edit payload with real-time validation
- Result Display: View success/failure, status code, duration
- Header Inspection: View exact request/response headers
- Signature Display: Copy signature for testing verification
Accessing the Test Interface
- Navigate to Admin → Webhooks
- Click on a webhook to view details
- Click "Test" button
- Use the interactive testing panel
Test Headers
Test requests include additional headers:
X-Webhook-Test: true- Indicates this is a test- Standard webhook headers apply
Delivery Monitoring
Delivery States
| Status | Description |
|---|---|
pending | Awaiting delivery or retry |
success | Delivered successfully (2xx response) |
failed | All retries exhausted |
Attempt History
Each delivery tracks individual attempts:
- Attempt number
- Timestamp
- Response status
- Response headers
- Response body
- Duration (ms)
- Error message (if any)
Retry Policy
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2h 36m |
After all retries fail, the delivery is marked as failed.
Technical Architecture
Models
WebhookEndpoint: app/Models/WebhookEndpoint.php
class WebhookEndpoint extends Model
{
protected $fillable = [
'name', 'url', 'client_id', 'events', 'secret',
'is_active', 'custom_headers', 'timeout_seconds',
'max_retries', 'log_payloads', 'created_by',
];
protected $casts = [
'events' => 'array',
'custom_headers' => 'array',
'is_active' => 'boolean',
'log_payloads' => 'boolean',
];
}
WebhookDelivery: app/Models/WebhookDelivery.php
class WebhookDelivery extends Model
{
protected $fillable = [
'webhook_endpoint_id', 'event_type', 'payload',
'request_headers', 'status', 'response_code',
'response_headers', 'response_body', 'duration_ms',
'attempts', 'error_message', 'next_retry_at',
];
public function attempts(): HasMany
{
return $this->hasMany(WebhookDeliveryAttempt::class);
}
}
WebhookDeliveryAttempt: app/Models/WebhookDeliveryAttempt.php
class WebhookDeliveryAttempt extends Model
{
protected $fillable = [
'webhook_delivery_id', 'attempt_number',
'request_headers', 'response_status',
'response_headers', 'response_body',
'duration_ms', 'error_message', 'attempted_at',
];
}
Services
WebhookService: app/Services/WebhookService.php
- Endpoint CRUD operations
- Webhook dispatching
- Stats calculation
WebhookTesterService: app/Services/Webhooks/WebhookTesterService.php
- Send test webhooks
- Generate sample payloads
- Provide signature documentation
Database Tables
Table: webhook_endpoints
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
name | string | Webhook name |
url | string | Endpoint URL |
client_id | bigint | Optional client FK |
events | json | Subscribed events |
secret | string | Signing secret |
custom_headers | json | Custom headers |
timeout_seconds | int | Request timeout |
max_retries | int | Max retry attempts |
log_payloads | boolean | Log payloads |
is_active | boolean | Enabled status |
created_by | bigint | Creator user FK |
Table: webhook_deliveries
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
webhook_endpoint_id | bigint | Endpoint FK |
event_type | string | Event type |
payload | json | Sent payload |
request_headers | json | Request headers |
status | string | pending/success/failed |
response_code | int | HTTP status |
response_headers | json | Response headers |
response_body | text | Response body |
duration_ms | int | Response time |
attempts | int | Attempt count |
error_message | text | Error details |
next_retry_at | timestamp | Next retry time |
Table: webhook_delivery_attempts
| Column | Type | Description |
|---|---|---|
id | bigint | Primary key |
webhook_delivery_id | uuid | Delivery FK |
attempt_number | int | Attempt sequence |
request_headers | json | Request headers |
response_status | int | HTTP status |
response_headers | json | Response headers |
response_body | text | Response body |
duration_ms | int | Duration |
error_message | text | Error details |
attempted_at | timestamp | Attempt time |
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Background Jobs | Queued delivery |
| Authentication | Admin access required |
Complementary Features
| Feature | Description |
|---|---|
| Notifications | Internal notifications |
| API Guide | API integration |
| Activity Logging | Webhook event logs |
Best Practices
For Administrators
- Use HTTPS URLs for security
- Store secrets securely in receiving system
- Monitor delivery logs for failures
- Use interactive testing before going live
- Review documentation page for signature examples
For Developers
- Always verify signatures before processing
- Check timestamp to prevent replay attacks
- Respond quickly (< 30 seconds)
- Return 2xx for success to prevent retries
- Queue heavy processing after acknowledgment
- Implement idempotency using event ID
Troubleshooting
| Issue | Solution |
|---|---|
| Webhook not triggering | Check is_active and event subscription |
| Signature invalid | Verify secret and timestamp format |
| Timeouts | Increase timeout or process async |
| Retries failing | Check endpoint availability |
| Test fails but real events work | Check X-Webhook-Test header handling |
See Also
- API Guide - REST API
- Notifications - Internal notifications
- Background Jobs - Queue processing