Skip to main content

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

FieldTypeDescription
namestringDisplay name for the webhook (1-255 characters)
urlstringHTTPS endpoint URL to receive events
eventsarrayEvent types to subscribe to (see Webhook Events)
activebooleanWhether 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:

HeaderDescription
Content-TypeAlways application/json
X-Webhook-SignatureHMAC-SHA256 hex digest of {timestamp}.{payload}
X-Webhook-TimestampUnix timestamp of the delivery
X-TrustGate-SignatureGitHub-style signature: sha256={hex_digest}
User-AgentTrustGate-Webhook/1.0

The integrations webhook system also sends:

HeaderDescription
X-Webhook-Signaturesha256={HMAC-SHA256 hex digest of JSON payload}
X-Webhook-EventEvent type string
X-Webhook-IdWebhook 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:

AttemptDelay After Failure
1Immediate
230 seconds
35 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 2xx status 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:

StatusMeaning
activeHealthy and delivering events
pausedManually deactivated (active: false)
degradedLast delivery failed, but under the failure threshold
failing10 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

  1. Return 200 quickly -- Process webhook payloads asynchronously. Return a 2xx response immediately, then process the event in a background job.
  2. Handle duplicates -- Use the event_id field for idempotency. The same event may be delivered more than once.
  3. Verify signatures -- Always validate the HMAC signature before processing.
  4. Use HTTPS -- Webhook endpoints should use HTTPS in production.
  5. Log deliveries -- Keep a record of received webhooks for debugging.
  6. Handle unknown events gracefully -- New event types may be added. Do not error on unrecognized event_type values.

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