Send GaaS alerts to your SIEM
Point your organization's own webhooks at Splunk, Microsoft Sentinel, Datadog, or any SIEM that accepts HTTPS. Every delivery is signed, retried, and recorded if it fails.
How it works
When GaaS blocks or escalates an agent's action, it can POST the event to an HTTPS address that your organization registers. That is the whole integration: there is no separate SIEM feed to switch on. Each organization has its own webhooks, its own signing secrets and its own delivery records, so your events never share a pipe with anyone else's.
Some SIEMs accept GaaS's POST exactly as it is sent. Others want their own envelope, or an auth header GaaS doesn't send. For those, a small adapter sits in between: it checks the signature, reshapes the event and forwards it.
| SIEM | Straight from GaaS? | Why |
|---|---|---|
| Datadog | Yes | The logs intake takes any JSON object and accepts the API key as a URL parameter. |
| Splunk (HTTP Event Collector) | No, use the adapter | HEC wants an Authorization: Splunk <token> header and an {"event": …} envelope. |
| Microsoft Sentinel | No, use a Logic App or the adapter | The Logs Ingestion API needs a Microsoft Entra token and a JSON array. |
| Anything else | Usually via the adapter | Verify, reshape, forward. |
Which events to send
| Event | Sent when | Worth sending to a SIEM? |
|---|---|---|
decision.blocked | GaaS blocked an agent's action | Yes, the main signal |
decision.escalated | GaaS held an action for human review | Yes |
escalation.decided | A reviewer approved or rejected a held action | Usually, it closes the loop on an escalation |
escalation.timed_out | Nobody reviewed a held action in time | Yes |
escalation.cancelled, escalation.reassigned | An escalation was cancelled, or handed to other reviewers | Optional |
quota.exceeded | Your organization reached its hard usage limit, so GaaS is refusing new intents | Yes |
decision.overridden | A person approved a blocked action, so the agent's next retry of that same action is approved once | Yes: a person set a block aside |
decision.approved | An action was approved, with or without modifications | Usually not: it fires for every approved action |
Decision events are sent in every pipeline mode. The data.pipeline_mode field tells you which one: live, shadow or test. Most SIEM rules should act on live only.
rate_limit.exceeded, observation.recorded, policy.calibrated and pattern.detected, but the hosted service does not currently send them. Don't build alerts on them.
Register a webhook
Use an API key with the admin role. List only the events you want; if you leave out event_types, the webhook receives every event type.
curl -X POST https://api.gaas.is/v1/escalations/webhooks \
-H "X-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://siem-adapter.example.com/gaas",
"event_types": ["decision.blocked", "decision.escalated", "escalation.timed_out", "quota.exceeded"],
"description": "SIEM forwarder"
}'
GaaS answers 201 Created:
{
"webhook": {
"id": "wh_81af440d0481",
"created_at": "2026-09-28T17:52:54.924800Z",
"url": "https://siem-adapter.example.com/gaas",
"organization_id": "org_acme",
"escalation_id": null,
"event_types": [
"decision.blocked",
"decision.escalated",
"escalation.timed_out",
"quota.exceeded"
],
"secret": "whsec_5b0e6c1d9a8f4e2b8c7d6e5f4a3b2c1d",
"active": true,
"description": "SIEM forwarder"
}
}
secretis generated by GaaS and shown in full only in this response. Listing your webhooks later shows"***". Store it where your adapter can read it. To rotate it, register a new webhook, switch your adapter over, then delete the old one.- Leave out
escalation_id. A webhook tied to one escalation only hears about that escalation, and never receivesdecision.*orquota.exceededevents. urlmust be HTTPS and publicly reachable, with nouser:password@in it. Private, loopback and cloud-metadata addresses are refused. A query string is fine, and the Datadog and Logic App options below rely on one.- Anyone in your organization who can list webhooks sees the full URL.
GET /v1/escalations/webhooksmasks the secret, not the URL, so a key or signature inside the URL is visible to them too. - An unknown event type or a refused URL gets
400with"code": "validation_error". Send anIdempotency-Keyheader if you might repeat the call.
To see what is registered, call GET /v1/escalations/webhooks. To stop deliveries, call DELETE /v1/escalations/webhooks/{webhook_id} with an admin key.
What GaaS sends
Each delivery is an HTTPS POST with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-GaaS-Signature | sha256= followed by the hex HMAC-SHA256 of the body (see Verify the signature) |
X-GaaS-Event | The event type, for example decision.blocked |
X-GaaS-Delivery | The delivery ID (dlv_…). Automatic retries of one delivery reuse it. |
X-GaaS-Retry | true, only on a manual retry |
A decision.blocked body, indented here for reading:
{
"data": {
"decision_id": "dec_0f0de7b67fcd4e1487fe6bc5030eacf6",
"human_override_id": null,
"intent_id": "int_85e46e12d8d949ff9c207bffac3afca5",
"pipeline_mode": "live",
"risk_score": 0.562,
"timestamp": "2026-09-28T17:52:54.955690+00:00",
"verdict": "block"
},
"decision_id": "dec_0f0de7b67fcd4e1487fe6bc5030eacf6",
"escalation_id": null,
"escalation_status": null,
"event_type": "decision.blocked",
"intent_id": "int_85e46e12d8d949ff9c207bffac3afca5",
"observation_id": null,
"organization_id": "org_acme",
"pattern_id": null,
"policy_id": null,
"timestamp": "2026-09-28T17:52:54.959796Z",
"webhook_id": "wh_81af440d0481"
}
On the wire the body is compact JSON with its keys in alphabetical order. Always check the signature against the bytes you received, never against JSON you have parsed and re-encoded.
| Field | Meaning |
|---|---|
event_type | Same as the X-GaaS-Event header |
timestamp | When GaaS built this delivery (UTC) |
webhook_id, organization_id | Which of your webhooks, and which organization |
intent_id, decision_id | Set on decision events; null otherwise |
escalation_id, escalation_status | Set on escalation events; null otherwise |
data |
Decision events: verdict (approve, approve_modified, escalate or block, always lowercase), risk_score (0 to 1), pipeline_mode, the decision's own timestamp, and human_override_id — set when a person's approval turned a block into this approval, otherwise null.decision.overridden: intent_id and decision_id of the blocked action, override_id, agent_id, approved_by, approved_at, expires_at and policies_overridden.Escalation events: the full escalation record. quota.exceeded: plan_id, included_actions, used_actions, hard_limit, batch_size, timestamp.
|
observation_id, policy_id, pattern_id | Always null in the events listed above |
GET /v1/intents/{intent_id}/audit, using any API key from your organization.
Verify the signature
The signature in X-GaaS-Signature is built like this:
- Algorithm: HMAC-SHA256.
- Key: your webhook's secret, the whole
whsec_…string, as UTF-8 bytes. - Message: the raw request body, byte for byte.
- Header value:
sha256=followed by the lowercase hex digest.
import hashlib
import hmac
def is_from_gaas(headers: dict[str, str], raw_body: bytes, secret: str) -> bool:
"""True if X-GaaS-Signature matches the exact bytes that were received."""
received = next((v for k, v in headers.items() if k.lower() == "x-gaas-signature"), "")
expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)
The signature covers the body only; it carries no timestamp. To stop someone replaying a captured request, remember the X-GaaS-Delivery IDs you have already accepted and ignore repeats. Automatic retries reuse the same ID and the same bytes, so this also removes duplicates.
Retries and failed deliveries
- GaaS makes up to 3 attempts per delivery. It waits 5 seconds after the first failure and 25 seconds after the second.
- Each attempt waits up to 10 seconds for your answer. Any
2xxstatus counts as delivered. Any other status, a timeout or a refused connection counts as a failure. - After the third failure the delivery is marked failed and kept, so you can find it.
List failed deliveries
Use an operator or admin key. Add ?webhook_id=wh_… to narrow the list to one webhook.
curl https://api.gaas.is/v1/escalations/webhooks/deliveries/failed \
-H "X-API-Key: $OPERATOR_API_KEY"
{
"deliveries": [
{
"id": "dlv_d4cadf8e3fc6",
"webhook_id": "wh_81af440d0481",
"event_type": "decision.blocked",
"status": "failed",
"attempts": 3,
"max_attempts": 3,
"last_attempt_at": "2026-09-28T17:55:31.755677Z",
"response_status": 503,
"error": null,
"created_at": "2026-09-28T17:55:31.737650Z",
"escalation_id": null,
"intent_id": "int_bc99eb55235c40d997a6cef7df9dc321",
"entity_id": "int_bc99eb55235c40d997a6cef7df9dc321"
}
],
"total": 1
}
Retry one
curl -X POST https://api.gaas.is/v1/escalations/webhooks/deliveries/dlv_d4cadf8e3fc6/retry \
-H "X-API-Key: $OPERATOR_API_KEY"
The answer is {"success": true|false, "delivery_id": "…", "message": "…"}. A delivery that doesn't exist, or hasn't failed, gets 404.
X-GaaS-Retry: true, the same event type, and data.original_delivery_id. Its intent_id and decision_id are null. To recover what was missed, take intent_id from the failed-delivery record and fetch GET /v1/intents/{intent_id}/audit. For a whole time window, use GET /v1/audit/export/stream?start_date=…&end_date=… (operator or admin key), which returns one audit record per line.
Webhooks are a live feed, not the system of record. The audit trail is. If your SIEM has to be complete, reconcile it against the audit export on a schedule.
Datadog: send directly
Register this as the webhook URL. It is shown for the US1 site; use the intake host for your own Datadog site.
https://http-intake.logs.datadoghq.com/api/v2/logs?dd-api-key=YOUR_DATADOG_API_KEY&ddsource=gaas
- Datadog's logs intake accepts a single JSON object and answers
202, which GaaS counts as delivered. The GaaS fields arrive as log attributes. - The trade-off: your Datadog API key sits in the URL, where anyone in your GaaS organization who lists webhooks can read it, and Datadog does not check
X-GaaS-Signature. Use a dedicated key you can revoke. - If you want the signature checked, run the adapter instead and send the key in a
DD-API-KEYheader.
Splunk: HTTP Event Collector, via the adapter
HEC's /services/collector/event endpoint expects an Authorization: Splunk <token> header and the event inside an "event" key. GaaS sends neither. Its body has no "event" key, so HEC refuses it with status code 12, "Event field is required" (HTTP 400).
HEC can take the token in the query string instead, but only if query-string authentication is enabled for that token (allowQueryStringAuth = true; on Splunk Cloud Platform, through a support case). The envelope is still required, so the adapter is the practical route. Give it this wrap function:
from datetime import datetime
def to_splunk(event: dict) -> dict:
return {
"time": datetime.fromisoformat(event["timestamp"]).timestamp(), # UNIX time (Python 3.11+ parses the Z)
"source": "gaas",
"sourcetype": "gaas:webhook",
"event": event,
}
# SIEM_URL = "https://YOUR-HEC-HOST:8088/services/collector/event"
# SIEM_HEADERS = {"Authorization": "Splunk YOUR_HEC_TOKEN"}
Microsoft Sentinel: a Logic App, or the adapter
Sentinel reads from a Log Analytics workspace. The supported way to push custom events into one is the Azure Monitor Logs Ingestion API, and that API needs a Microsoft Entra bearer token, a data collection rule (DCR) and a JSON array body. A GaaS webhook can't supply any of those, so something sits in between.
Both options need:
- A custom table in the workspace (its name ends in
_CL). If you create it in the Azure portal from a sample GaaS payload, the portal creates the DCR, and any transformation it needs, for you. - The Monitoring Metrics Publisher role on the DCR for whatever identity sends the data.
Option A: Logic App (no code)
- Start a workflow with the When a HTTP request is received trigger and save it. Copy the URL it generates. The URL contains a shared access signature (
sig=…), so treat it as a secret. Register it as your GaaS webhook URL. - Leave out a Response action. The trigger then answers
202straight away, which GaaS counts as delivered. - Add an HTTP action that POSTs to
{endpoint}/dataCollectionRules/{dcrImmutableId}/streams/{streamName}?api-version=2023-01-01, with authentication set to the Logic App's managed identity and Audience set tohttps://monitor.azure.com. Send the trigger body, wrapped in a JSON array, asapplication/json.
This option does not check X-GaaS-Signature. The secret sig in the trigger URL is what keeps others out, and anyone in your GaaS organization who lists webhooks can see that URL.
Option B: the adapter, with the signature checked
Run the adapter (an Azure Function works) and, instead of forwarding over plain HTTP, upload with the Azure Monitor Ingestion client library (pip install azure-monitor-ingestion azure-identity):
from azure.identity import DefaultAzureCredential
from azure.monitor.ingestion import LogsIngestionClient
client = LogsIngestionClient(endpoint=YOUR_DCR_OR_DCE_ENDPOINT, credential=DefaultAzureCredential())
client.upload(rule_id=YOUR_DCR_IMMUTABLE_ID, stream_name=YOUR_STREAM_NAME, logs=[event])
A tiny adapter for any SIEM
This function checks one GaaS delivery and forwards it. Call it from any web framework or serverless function: pass the request headers and the exact raw body bytes, and answer GaaS with the status it returns. It needs only the Python standard library.
import hashlib
import hmac
import json
import os
import urllib.request
SECRET = os.environ["WEBHOOK_SECRET"] # the whsec_… value from registration
SIEM_URL = os.environ["SIEM_URL"] # where your SIEM accepts events
SIEM_HEADERS = json.loads(os.environ.get("SIEM_HEADERS", "{}")) # e.g. {"Authorization": "Splunk …"}
def handle(headers: dict[str, str], raw_body: bytes, wrap=lambda event: event) -> int:
"""Verify one GaaS delivery and forward it. Returns the status to answer GaaS with."""
h = {k.lower(): v for k, v in headers.items()}
expected = "sha256=" + hmac.new(SECRET.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(h.get("x-gaas-signature", ""), expected):
return 401 # not from GaaS: refuse it
if h.get("x-gaas-retry") == "true":
return 200 # manual-retry notice: no event data inside
body = json.dumps(wrap(json.loads(raw_body))).encode("utf-8")
request = urllib.request.Request(SIEM_URL, data=body, method="POST",
headers={"Content-Type": "application/json", **SIEM_HEADERS})
try:
with urllib.request.urlopen(request, timeout=5) as response:
return 200 if 200 <= response.status < 300 else 502
except OSError:
return 502 # SIEM unreachable or refused: GaaS retries
- Splunk:
wrap=to_splunkfrom above, with the HEC URL andAuthorizationheader. - Datadog, signature checked:
SIEM_URL=https://http-intake.logs.datadoghq.com/api/v2/logs(or your site's intake host) andSIEM_HEADERS={"DD-API-KEY": "…"}. - Sentinel: replace the forwarding lines with the
client.upload(…)call above. - Answer quickly. GaaS waits up to 10 seconds; the example gives your SIEM 5. Returning an error makes GaaS try again.
- Keep a short-lived record of
X-GaaS-DeliveryIDs you have forwarded, and skip repeats.
Vendor sources
The vendor details on this page were checked against these pages on 2026-09-28:
- Splunk: Format events for HTTP Event Collector (endpoint,
Authorization: Splunkheader, envelope keys, query-string token authentication) - Splunk: Troubleshoot HTTP Event Collector (status code 12, "Event field is required")
- Datadog: Send logs API (intake endpoint,
DD-API-KEYheader, JSON body,202response) - Datadog: Apigee log collection guide (intake URL with
dd-api-keyandddsourcequery parameters) - Microsoft: Logs Ingestion API in Azure Monitor (URI format, bearer token, JSON array body, Monitoring Metrics Publisher role, portal-created DCRs)
- Microsoft: Receive and respond to inbound HTTPS calls (Request trigger,
202without a Response action) - Microsoft: Create callable workflows (shared access signature in the trigger URL)
- Microsoft: Authenticate with managed identities (HTTP action, Audience)
- Microsoft: Azure Monitor Ingestion client library for Python (
LogsIngestionClient.upload)
Related Pages
- Webhooks — Every event type, payload and header in one place
- Connectors — Reading risk context from your SIEM during evaluation
- Advanced Features — Signing, public timestamps and other audit-trail features
- Authentication — API keys and roles