Webhooks allow external systems to receive real-time notifications when events occur in TGM Expert. When an event is triggered (e.g., an inspection is created), TGM Expert sends an HTTP POST request to your configured URL with details about the event.
Table of Contents¶
- Overview
- Multi-Tenancy Context
- Configuration
- Event Types
- Payload Structure
- Security
- Delivery & Retries
- Admin API
- Best Practices
Overview¶
Key Features¶
- 70+ Event Types - Cover all major entities (inspections, units, alerts, etc.)
- Wildcard Subscription - Subscribe to all events with
* - HMAC-SHA256 Signatures - Cryptographic payload verification
- Automatic Retries - Exponential backoff on failures
- Delivery Tracking - Full history of all deliveries
- Auto-Disable - Automatically disable after consecutive failures
- Multi-Tenant Scope - Webhooks are per-company within each client
Multi-Tenancy Context¶
Webhooks operate within TGM's multi-tenant architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Platform Level │
│ Webhook processing engine (shared) │
└─────────────────────────────────────────────────────────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Client A │ │ Client B │ │ Client C │
│ Database │ │ Database │ │ Database │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ webhooks │ │ webhooks │ │ webhooks │
│ (table) │ │ (table) │ │ (table) │
│ │ │ │ │ │
│ Webhook A-1 │ │ Webhook B-1 │ │ Webhook C-1 │
│ (company 1) │ │ (company 1) │ │ (company 1) │
└───────────────┘ └───────────────┘ └───────────────┘
Key Points¶
- Webhooks are per-company - Each company can have its own webhook configurations
- Events trigger within client context - Only events from that client's database trigger webhooks
- Webhook configs stored per-client - The
webhookstable is in each client's database
Webhook Payload Includes Client Info¶
{
"event": "inspection.created",
"timestamp": "2026-02-05T10:30:00Z",
"client": {
"id": "acme-corp",
"name": "Acme Corporation"
},
"company": {
"id": 1,
"name": "Acme HQ"
},
"data": { ... }
}
How It Works¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ TGM Expert │────>│ Webhook │────>│ Your Server │
│ (Event) │ │ Service │ │ (Endpoint) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 1. Event occurs │ │
│──────────────────>│ │
│ │ 2. POST payload │
│ │──────────────────>│
│ │ │
│ │ 3. 2xx response │
│ │<──────────────────│
│ │ │
Configuration¶
Creating a Webhook¶
curl -X POST https://api.tgm-expert.com/api/admin/webhooks \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Integration Webhook",
"description": "Sends events to our internal system",
"url": "https://your-server.com/webhooks/tgm",
"companyId": 1,
"events": ["inspection.created", "inspection.updated", "alert.created"],
"httpMethod": "POST",
"contentType": "application/json",
"timeoutMs": 30000,
"maxRetries": 3,
"verifySsl": true,
"autoDisableAfterFailures": 10
}'
Response¶
{
"data": {
"id": 1,
"name": "My Integration Webhook",
"url": "https://your-server.com/webhooks/tgm",
"isActive": true,
"events": ["inspection.created", "inspection.updated", "alert.created"],
"hasSecret": true,
"secretPreview": "whsec_abc12345...xyz9",
"createdAt": "2026-02-01T00:00:00Z"
},
"meta": {
"message": "Webhook created. Save the secret key - it won't be shown again."
}
}
Important: Save the
secretPreviewvalue immediately. The full secret is only shown once during creation.
Configuration Options¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | - | Webhook name (max 100 chars) |
description |
string | No | - | Description (max 500 chars) |
url |
string | Yes | - | Endpoint URL (https recommended) |
companyId |
number | No | null | Company ID (null for system-wide) |
events |
string[] | No | [] | Event codes to subscribe to |
customHeaders |
object | No | {} | Custom HTTP headers |
httpMethod |
string | No | POST | HTTP method (POST, PUT, PATCH) |
contentType |
string | No | application/json | Content-Type header |
timeoutMs |
number | No | 30000 | Request timeout in milliseconds |
maxRetries |
number | No | 3 | Maximum retry attempts |
verifySsl |
boolean | No | true | Verify SSL certificates |
autoDisableAfterFailures |
number | No | 10 | Auto-disable threshold (0 = never) |
Event Types¶
Event Naming Convention¶
Events follow the pattern: {resource}.{action}
Examples:
- inspection.created - Inspection was created
- unit.updated - Unit was updated
- alert.resolved - Alert was resolved
Available Events¶
Asset Events¶
| Event | Description |
|---|---|
location.created |
Location/plant created |
location.updated |
Location/plant updated |
location.deleted |
Location/plant deleted |
unit.created |
Unit created |
unit.updated |
Unit updated |
unit.deleted |
Unit deleted |
component.created |
Component created |
component.updated |
Component updated |
component.deleted |
Component deleted |
Maintenance Events¶
| Event | Description |
|---|---|
inspection.created |
Inspection created |
inspection.updated |
Inspection updated |
inspection.deleted |
Inspection deleted |
intervention.created |
Intervention created |
intervention.updated |
Intervention updated |
intervention.deleted |
Intervention deleted |
work_order.created |
Work order created |
work_order.updated |
Work order updated |
work_order.deleted |
Work order deleted |
work_order.status_changed |
Work order status changed |
failure.created |
Failure recorded |
failure.updated |
Failure updated |
failure.deleted |
Failure deleted |
Monitoring Events¶
| Event | Description |
|---|---|
alert.created |
Alert triggered |
alert.updated |
Alert updated |
alert.acknowledged |
Alert acknowledged |
alert.resolved |
Alert resolved |
sensor.created |
Sensor configured |
sensor.updated |
Sensor updated |
sensor.deleted |
Sensor removed |
sensor.reading_received |
Sensor reading received |
Lifecycle Events¶
| Event | Description |
|---|---|
asset_lifecycle.created |
Lifecycle record created |
asset_lifecycle.updated |
Lifecycle updated |
asset_lifecycle.deleted |
Lifecycle deleted |
maintenance_plan.created |
Maintenance plan created |
maintenance_plan.updated |
Maintenance plan updated |
maintenance_plan.due |
Maintenance is due |
spare_part.low_stock |
Spare part stock is low |
Document Events¶
| Event | Description |
|---|---|
document.created |
Document uploaded |
document.updated |
Document updated |
document.deleted |
Document deleted |
drawing.created |
Drawing uploaded |
drawing.updated |
Drawing updated |
drawing.deleted |
Drawing deleted |
Communication Events¶
| Event | Description |
|---|---|
notification.created |
Notification created |
comment.created |
Comment added |
comment.updated |
Comment updated |
comment.deleted |
Comment deleted |
todo.created |
Todo created |
todo.updated |
Todo updated |
todo.completed |
Todo completed |
Wildcard¶
| Event | Description |
|---|---|
* |
Subscribe to ALL events |
Payload Structure¶
Standard Payload¶
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"event": "inspection.created",
"timestamp": "2026-02-01T12:30:45.123Z",
"webhookId": 1,
"apiVersion": "1.0",
"data": {
"id": 12345,
"type": "Inspection",
"attributes": {
"id": 12345,
"title": "Monthly Turbine Inspection",
"status": "pending",
"scheduledDate": "2026-02-15",
"unit": {
"id": 100,
"name": "Turbine Unit 1"
},
"inspector": {
"id": 50,
"name": "John Smith"
},
"createdAt": "2026-02-01T12:30:45Z",
"updatedAt": "2026-02-01T12:30:45Z"
},
"previous": null
},
"metadata": null
}
Update Payload (with previous state)¶
{
"eventId": "550e8400-e29b-41d4-a716-446655440001",
"event": "inspection.updated",
"timestamp": "2026-02-01T14:00:00.000Z",
"webhookId": 1,
"apiVersion": "1.0",
"data": {
"id": 12345,
"type": "Inspection",
"attributes": {
"id": 12345,
"title": "Monthly Turbine Inspection",
"status": "completed",
"completedAt": "2026-02-01T14:00:00Z"
},
"previous": {
"status": "in_progress"
}
}
}
Delete Payload¶
{
"eventId": "550e8400-e29b-41d4-a716-446655440002",
"event": "inspection.deleted",
"timestamp": "2026-02-01T15:00:00.000Z",
"webhookId": 1,
"apiVersion": "1.0",
"data": {
"id": 12345,
"type": "Inspection",
"attributes": {
"id": 12345,
"title": "Monthly Turbine Inspection"
}
}
}
Security¶
Signature Verification¶
Every webhook request includes an HMAC-SHA256 signature in the X-TGM-Signature header. Use this to verify the payload authenticity.
Signature Header Format¶
X-TGM-Signature: sha256=5d41402abc4b2a76b9719d911017c592
Verification Example (Node.js)¶
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express middleware
app.post('/webhooks/tgm', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-tgm-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process event...
res.status(200).send('OK');
});
Verification Example (Python)¶
import hmac
import hashlib
def verify_signature(payload: str, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.route('/webhooks/tgm', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-TGM-Signature')
payload = request.get_data(as_text=True)
if not verify_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
event = request.get_json()
# Process event...
return 'OK', 200
Request Headers¶
| Header | Description |
|---|---|
X-TGM-Signature |
HMAC-SHA256 signature |
X-TGM-Event |
Event type (e.g., inspection.created) |
X-TGM-Delivery |
Unique delivery ID |
X-TGM-Timestamp |
Unix timestamp of request |
Content-Type |
application/json |
Delivery & Retries¶
Retry Schedule¶
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
After all retries are exhausted, the delivery is marked as failed.
Success Criteria¶
A delivery is considered successful when: - HTTP status code is 2xx (200-299) - Response is received within the timeout period
Auto-Disable¶
If a webhook fails autoDisableAfterFailures consecutive times (default: 10), it is automatically disabled. The webhook can be re-enabled via the admin API, which resets the failure counter.
Viewing Delivery History¶
curl -X GET "https://api.tgm-expert.com/api/admin/webhooks/1/deliveries?limit=10" \
-H "Authorization: Bearer $JWT_TOKEN"
Response:
{
"data": [
{
"id": 100,
"webhookId": 1,
"eventType": "inspection.created",
"status": "SUCCESS",
"attemptCount": 1,
"responseStatus": 200,
"durationMs": 150,
"createdAt": "2026-02-01T12:30:45Z"
},
{
"id": 99,
"webhookId": 1,
"eventType": "alert.created",
"status": "FAILED",
"attemptCount": 3,
"responseStatus": 500,
"errorMessage": "Internal Server Error",
"durationMs": 30000,
"createdAt": "2026-02-01T12:00:00Z"
}
]
}
Admin API¶
Endpoints¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/webhooks/event-types |
List all event types |
| GET | /api/admin/webhooks/company/{id} |
List company webhooks |
| GET | /api/admin/webhooks/{id} |
Get webhook details |
| POST | /api/admin/webhooks |
Create webhook |
| PUT | /api/admin/webhooks/{id} |
Update webhook |
| DELETE | /api/admin/webhooks/{id} |
Delete webhook |
| POST | /api/admin/webhooks/{id}/enable |
Enable webhook |
| POST | /api/admin/webhooks/{id}/disable |
Disable webhook |
| POST | /api/admin/webhooks/{id}/regenerate-secret |
Regenerate secret |
| POST | /api/admin/webhooks/{id}/test |
Send test ping |
| GET | /api/admin/webhooks/{id}/deliveries |
Get delivery history |
| GET | /api/admin/webhooks/deliveries/{id} |
Get delivery details |
| POST | /api/admin/webhooks/deliveries/{id}/retry |
Retry failed delivery |
Testing a Webhook¶
Send a test ping to verify your endpoint is working:
curl -X POST "https://api.tgm-expert.com/api/admin/webhooks/1/test" \
-H "Authorization: Bearer $JWT_TOKEN"
The test payload:
{
"eventId": "ping-1706745600000",
"event": "webhook.ping",
"timestamp": "2026-02-01T12:00:00Z",
"webhookId": 1,
"apiVersion": "1.0",
"data": {
"type": "WebhookTest",
"attributes": {
"message": "This is a test webhook delivery"
}
}
}
Best Practices¶
1. Always Verify Signatures¶
Never trust incoming webhooks without verifying the HMAC signature.
2. Respond Quickly¶
Return a 2xx response as quickly as possible. Process the webhook asynchronously if needed.
app.post('/webhooks/tgm', (req, res) => {
// Acknowledge immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body);
});
3. Handle Duplicates¶
The same event may be delivered multiple times. Use eventId for deduplication.
const processedEvents = new Set();
function handleWebhook(event) {
if (processedEvents.has(event.eventId)) {
return; // Already processed
}
processedEvents.add(event.eventId);
// Process event...
}
4. Use HTTPS¶
Always use HTTPS endpoints in production to ensure payload encryption in transit.
5. Monitor Failures¶
Regularly check your webhook's health via the admin API. Set up alerts for high failure rates.
6. Subscribe Only to Needed Events¶
Don't use * wildcard unless you truly need all events. Subscribe only to events you'll actually process.
7. Handle Payload Size¶
Payloads are limited to 1MB. For large entities, only essential fields are included.
Troubleshooting¶
Webhook Not Receiving Events¶
- Check if webhook is active:
GET /api/admin/webhooks/{id} - Check if subscribed to correct events: Verify
eventsarray - Check delivery history:
GET /api/admin/webhooks/{id}/deliveries - Send test ping:
POST /api/admin/webhooks/{id}/test
Signature Verification Failing¶
- Use the raw request body (before JSON parsing)
- Ensure you're using the correct secret
- Check for encoding issues (UTF-8)
- Use constant-time comparison to prevent timing attacks
High Failure Rate¶
- Check your endpoint's response time (should be < 30s)
- Ensure endpoint returns 2xx status
- Check server logs for errors
- Increase
timeoutMsif processing takes time
Rate Limits¶
Webhook deliveries are subject to rate limiting:
| Limit | Value |
|---|---|
| Max webhooks per company | 20 |
| Max events per second | 100 |
| Max payload size | 1 MB |
| Max retries | 3 |
| Delivery retention | 30 days |