A webhook posts your organization’s audit events to an HTTPS endpoint you own: each event as it happens, or every event of the day in one batch. You add one in the hub under Audit log → Webhooks, and the hub shows its signing secret once. Your endpoint’s job is to check that each request came from Trustaige, answer quickly, and ignore an event it has already seen.
- Trustaige to Your endpoint: POST the event, signed
- Your endpoint to Trustaige: 2xx within 10 seconds
- Trustaige to Your endpoint: on failure: again after 30 s, then 5 min
What each request carries
Every delivery is an HTTPS POST. Redirects are never followed, so give the
final address.
| Header | What it holds |
|---|---|
X-Webhook-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the signing secret |
X-Webhook-Signature-Old | The same, signed with the previous secret. Sent only while a rotation’s 24 hours run |
X-Event-ID | The audit event’s ID. Every retry of one event carries the same ID. A daily batch sends batch |
X-Event-Type | The event’s type, such as session.created. A daily batch sends scheduled.export |
X-Webhook-Delivery-ID | New for every attempt. Quote it when you ask about one delivery |
Content-Type | Depends on the format; see Formats |
User-Agent | Trustaige-Identity-Hub/1.0 |
Any headers you added to the webhook in the hub, such as an API token for your collector, are sent with every delivery, real-time or daily batch.
Verify the signature
Compute the HMAC-SHA256 of the request body exactly as it arrived, before
any JSON parsing, keyed with the signing secret as the hub showed it (use
the text itself; don’t hex-decode it). Prefix sha256= and compare it with
X-Webhook-Signature in constant time. Refuse the request if it doesn’t match.
import crypto from 'node:crypto';
// rawBody: the request body exactly as received, before any JSON parsing.
export function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// rawBody: the request body exactly as received, before any JSON parsing.
func verify(rawBody []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}import hashlib
import hmac
# raw_body: the request body exactly as received, before any JSON parsing.
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "") import crypto from 'node:crypto';
// rawBody: the request body exactly as received, before any JSON parsing.
export function verify(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// rawBody: the request body exactly as received, before any JSON parsing.
func verify(rawBody []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}import hashlib
import hmac
# raw_body: the request body exactly as received, before any JSON parsing.
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "") When you rotate the secret
Rotating in the hub shows a new secret once. For the next 24 hours every
delivery, real-time or daily batch, is signed with both: the new secret in
X-Webhook-Signature, the previous one in X-Webhook-Signature-Old. Accept a
request if either matches, give your endpoint the new secret, and the old one
stops on its own when the 24 hours end.
Retries and duplicates
Your endpoint has 10 seconds to answer. Any 2xx is a success; anything else,
a timeout, a redirect or a reply over 8 KB is a failure. A real-time event is
tried up to three times: at once, after 30 seconds, and after 5 minutes.
Because a slow 2xx can still be retried, the same event can arrive more than
once. Keep the X-Event-ID values you’ve handled and skip one you’ve seen.
When ten events in a row fail all three attempts, the webhook turns itself off and the hub says so. Fix your endpoint, send a test event from the webhook’s page, then turn it back on; that clears the count. Events that happened while it was off are not sent later. The audit log still has them, and you can export them.
Formats
Each webhook sends one format, chosen in the hub.
| Format | Body | Content-Type |
|---|---|---|
| JSON | One event as a JSON object | application/json |
| CEF | One event as an ArcSight CEF line | text/plain |
| LEEF | One event as an IBM QRadar LEEF line | text/plain |
| OCSF | One event as an OCSF JSON object | application/json |
A test event sent from the hub is a real delivery of type webhook.tested,
signed the same way, so it is the quickest check that your verification works.
Daily batches
A webhook set to deliver once a day sends one request every 24 hours instead of one per event.
- The body is newline-delimited: one event per line, each in the webhook’s
format, and the request’s
Content-Typeisapplication/x-ndjson. X-Event-Typeisscheduled.exportandX-Event-IDisbatch. To skip an event you’ve already stored, use the event’s own ID inside each line (idin JSON).- Each batch starts after the last event the previous one delivered, so none is skipped. A backlog goes out oldest first, at most 10,000 events to a request, in as many requests as it takes.
- A batch that fails is not retried at once: the next day’s run sends it again, from the same event.
- A batch carries the headers you added in the hub, so a collector that needs a token in a header (Splunk’s HTTP Event Collector, for example) can take batches as well as real-time deliveries.