Skip to content
Trustaige Docs

Receive webhooks

Accept your organization's audit events at your own endpoint. Verify each signature, drop retried duplicates, and read daily batches.

View as Markdown

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.

  1. Trustaige to Your endpoint: POST the event, signed
  2. Your endpoint to Trustaige: 2xx within 10 seconds
  3. 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.

HeaderWhat it holds
X-Webhook-Signaturesha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the signing secret
X-Webhook-Signature-OldThe same, signed with the previous secret. Sent only while a rotation’s 24 hours run
X-Event-IDThe audit event’s ID. Every retry of one event carries the same ID. A daily batch sends batch
X-Event-TypeThe event’s type, such as session.created. A daily batch sends scheduled.export
X-Webhook-Delivery-IDNew for every attempt. Quote it when you ask about one delivery
Content-TypeDepends on the format; see Formats
User-AgentTrustaige-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.

FormatBodyContent-Type
JSONOne event as a JSON objectapplication/json
CEFOne event as an ArcSight CEF linetext/plain
LEEFOne event as an IBM QRadar LEEF linetext/plain
OCSFOne event as an OCSF JSON objectapplication/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-Type is application/x-ndjson.
  • X-Event-Type is scheduled.export and X-Event-ID is batch. To skip an event you’ve already stored, use the event’s own ID inside each line (id in 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.