Webhooks
Webhooks notify your application in real-time when events occur in TrustGate, such as verification completions, screening results, or case updates.
How Webhooks Work
+-----------------------------------------------------------+
| TRUSTGATE |
| |
| Event occurs (e.g., applicant.reviewed) |
+---------------------------+-------------------------------+
|
| HTTP POST with HMAC signature
v
+-----------------------------------------------------------+
| YOUR ENDPOINT |
| |
| https://your-app.com/webhooks/trustgate |
+---------------------------+-------------------------------+
|
| Return 2xx within 30 seconds
v
+-----------------------------------------------------------+
| YOUR APPLICATION |
| |
| Update database, notify user, trigger workflow |
+-----------------------------------------------------------+
Creating Webhooks
Via API
curl -X POST https://api.bytrustgate.com/api/v1/integrations/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Production webhook",
"url": "https://your-app.com/webhooks/trustgate",
"events": [
"applicant.reviewed",
"screening.completed",
"document.verified",
"case.created"
],
"active": true
}'
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Display name for the webhook (1-255 characters) |
url | string | HTTPS endpoint URL to receive events |
events | array | Event types to subscribe to (see Webhook Events) |
active | boolean | Whether the webhook is active (default: true) |
Use "*" in the events array to subscribe to all event types.
Response
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Production webhook",
"url": "https://your-app.com/webhooks/trustgate",
"secret": "whsec_your_webhook_secret_here",
"events": ["applicant.reviewed", "screening.completed", "document.verified", "case.created"],
"active": true,
"created_at": "2026-02-04T14:00:00Z"
}
Important: The secret is only returned once at creation time. Store it securely -- you need it to verify webhook signatures.
Webhook Payload
Standard Envelope
All events delivered through the primary webhook service use this structure:
{
"event_type": "applicant.reviewed",
"event_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"timestamp": "2026-02-04T14:30:00Z",
"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"correlation_id": null,
"data": {
"applicant_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "approved",
"risk_score": 25
}
}
HTTP Headers
Every webhook delivery includes these headers for verification:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Webhook-Signature | HMAC-SHA256 hex digest of {timestamp}.{payload} |
X-Webhook-Timestamp | Unix timestamp of the delivery |
X-TrustGate-Signature | GitHub-style signature: sha256={hex_digest} |
User-Agent | TrustGate-Webhook/1.0 |
The integrations webhook system also sends:
| Header | Description |
|---|---|
X-Webhook-Signature | sha256={HMAC-SHA256 hex digest of JSON payload} |
X-Webhook-Event | Event type string |
X-Webhook-Id | Webhook configuration ID |
Verifying Signatures
Always verify webhook signatures to ensure the payload was sent by TrustGate and was not tampered with.
The primary webhook system signs with {timestamp}.{payload}:
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, headers, secret) {
const timestamp = headers['x-webhook-timestamp'];
const receivedSignature = headers['x-webhook-signature'];
// Reconstruct the signed payload
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/trustgate', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, req.headers, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
handleEvent(event);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
def verify_webhook_signature(raw_body: str, headers: dict, secret: str) -> bool:
timestamp = headers.get("x-webhook-timestamp")
received_signature = headers.get("x-webhook-signature")
# Reconstruct the signed payload
signed_payload = f"{timestamp}.{raw_body}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(received_signature, expected_signature)
@app.route("/webhooks/trustgate", methods=["POST"])
def webhook():
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(raw_body, request.headers, WEBHOOK_SECRET):
return "Invalid signature", 401
event = request.get_json()
handle_event(event)
return "OK", 200
Replay Protection
The X-Webhook-Timestamp header contains the Unix timestamp when the webhook was sent. To protect against replay attacks, reject any webhook where the timestamp is more than 5 minutes old:
const timestamp = parseInt(headers['x-webhook-timestamp']);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return res.status(401).send('Webhook timestamp too old');
}
Handling Events
Event Handler Example
function handleEvent(event) {
switch (event.event_type) {
case 'applicant.submitted':
// Applicant started verification
console.log(`Applicant ${event.data.applicant_id} submitted`);
break;
case 'applicant.reviewed':
handleReviewed(event.data);
break;
case 'applicant.verification_complete':
handleVerificationComplete(event.data);
break;
case 'screening.completed':
handleScreeningComplete(event.data);
break;
case 'document.verified':
handleDocumentVerified(event.data);
break;
case 'case.created':
handleCaseCreated(event.data);
break;
default:
console.log(`Unhandled event type: ${event.event_type}`);
}
}
function handleReviewed(data) {
// Update user status in your database
await db.users.update({
where: { externalId: data.external_id },
data: {
kycStatus: data.status,
riskLevel: data.risk_level,
}
});
if (data.status === 'approved') {
await activateAccount(data.applicant_id);
}
}
Retry Policy
Failed webhook deliveries are retried automatically with exponential backoff:
| Attempt | Delay After Failure |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
After 3 failed attempts, the delivery is marked as permanently failed.
Success Criteria
A webhook delivery is considered successful when your endpoint returns:
- An HTTP
2xxstatus code - Within 30 seconds
Failure Handling
- 4xx responses (client errors) are treated as permanent failures and are not retried.
- 5xx responses (server errors) are retried according to the schedule above.
- Timeouts (no response within 30 seconds) are retried.
- Connection errors are retried.
If a webhook endpoint accumulates 10 or more consecutive failures, its status changes to "failing" and delivery attempts may be paused.
Managing Webhooks
List Webhooks
curl -X GET "https://api.bytrustgate.com/api/v1/integrations/webhooks" \
-H "Authorization: Bearer YOUR_TOKEN"
Get a Single Webhook
curl -X GET "https://api.bytrustgate.com/api/v1/integrations/webhooks/{webhook_id}" \
-H "Authorization: Bearer YOUR_TOKEN"
Update Webhook
curl -X PUT "https://api.bytrustgate.com/api/v1/integrations/webhooks/{webhook_id}" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated webhook name",
"events": ["applicant.reviewed", "screening.completed"],
"active": true
}'
Updatable fields: name, url, events, active. Re-enabling a webhook (active: true) resets the failure count.
Delete Webhook
curl -X DELETE "https://api.bytrustgate.com/api/v1/integrations/webhooks/{webhook_id}" \
-H "Authorization: Bearer YOUR_TOKEN"
Deleting a webhook also removes all associated delivery logs.
List Available Events
curl -X GET "https://api.bytrustgate.com/api/v1/integrations/webhooks/events" \
-H "Authorization: Bearer YOUR_TOKEN"
Returns the list of event types you can subscribe to.
Testing Webhooks
Send Test Event
Send a test.ping event to verify your endpoint is reachable:
curl -X POST "https://api.bytrustgate.com/api/v1/integrations/webhooks/{webhook_id}/test" \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"success": true,
"response_code": 200,
"response_time_ms": 145,
"error_message": null
}
View Delivery History
curl -X GET "https://api.bytrustgate.com/api/v1/integrations/webhooks/{webhook_id}/logs?limit=50&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"items": [
{
"id": "e1f23456-7890-1234-5678-234567890123",
"event_type": "applicant.reviewed",
"response_code": 200,
"response_time_ms": 145,
"success": true,
"error_message": null,
"created_at": "2026-02-04T14:30:00Z",
"delivered_at": "2026-02-04T14:30:00Z"
}
],
"total": 1
}
Webhook Status
Each webhook has a computed status based on its health:
| Status | Meaning |
|---|---|
active | Healthy and delivering events |
paused | Manually deactivated (active: false) |
degraded | Last delivery failed, but under the failure threshold |
failing | 10 or more consecutive failures |
Session-Level Webhooks
When creating a verification session via the API, you can specify a webhook_url to receive a session.completed event for that specific session, independent of your tenant-level webhook configuration:
curl -X POST https://api.bytrustgate.com/api/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "user_123",
"webhook_url": "https://your-app.com/webhooks/session-complete",
"redirect_url": "https://your-app.com/verification-done"
}'
Best Practices
- Return 200 quickly -- Process webhook payloads asynchronously. Return a 2xx response immediately, then process the event in a background job.
- Handle duplicates -- Use the
event_idfield for idempotency. The same event may be delivered more than once. - Verify signatures -- Always validate the HMAC signature before processing.
- Use HTTPS -- Webhook endpoints should use HTTPS in production.
- Log deliveries -- Keep a record of received webhooks for debugging.
- Handle unknown events gracefully -- New event types may be added. Do not error on unrecognized
event_typevalues.
Idempotency Example
app.post('/webhooks/trustgate', async (req, res) => {
const eventId = req.body.event_id;
// Check if already processed
const existing = await db.webhookEvents.findUnique({
where: { eventId }
});
if (existing) {
return res.status(200).send('Already processed');
}
// Process and record
await processEvent(req.body);
await db.webhookEvents.create({
data: { eventId, processedAt: new Date() }
});
res.status(200).send('OK');
});
Next Steps
- Webhook Events -- Complete event reference with payload details
- SDK Integration -- Client SDK guide
- API Keys -- Authentication and key management