Webhook reference
Technical reference for receiving System webhooks: the request the platform sends, how to verify it, what a GitLab pipeline receives, delivery semantics, and the REST API for managing webhooks. For configuring webhooks in the web app, see Webhooks. For enabling the feature on a deployment, see Enabling System Webhooks.
Events
| Event type | Fires when | Idempotency key (id) |
|---|---|---|
system.snapshot.created | A commit (snapshot) is recorded on any branch of the System | evt_<commit id> |
system.change_request.opened | A change request is opened for the first time (reopening does not fire) | evt_<change request id> |
id is stable for a given event, so a receiver can deduplicate if it sees the same event more than once, for example after a retry whose acknowledgement was lost. Deduplicate on id, not on the delivery ID: each webhook subscribed to the event gets its own delivery.
system.snapshot.created does not say which branch the commit was recorded on.
HTTP targets
Request
Each delivery is an HTTP POST to the webhook's URL with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
Istari-Event | The event type, for example system.snapshot.created |
Istari-Delivery | The delivery ID. Retries of the same delivery reuse it |
Istari-Signature | sha256=<hex>: the HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret |
Answer with any 2xx status within 10 seconds to acknowledge the delivery. Anything else is retried (see Delivery and retries). Redirects are not followed.
Payload
Every event shares one envelope; data differs per event.
{
"id": "evt_5f0c3a4e-…",
"event_type": "system.snapshot.created",
"occurred_at": "2026-09-30T14:03:12.481920+00:00",
"istari_instance": "https://istari.example.com",
"system": {
"id": "dd603c8e-…",
"name": "Trailblazer UAV Airframe",
"tenant_id": "3b1e…"
},
"actor": {
"user_id": "9a7d…",
"email": "engineer@example.com"
},
"data": {}
}
| Field | Description |
|---|---|
id | Stable idempotency key for the event |
event_type | One of the event types above |
occurred_at | When the event occurred, ISO 8601, UTC |
istari_instance | The web app URL of the platform that sent the event, as configured by your IT administrator |
system | The System the event belongs to: id, name, and the owning organization's tenant_id |
actor | The user whose action caused the event: user_id and email (email may be null) |
data | Event-specific fields, below |
system.snapshot.created
"data": {
"snapshot_id": "5f0c3a4e-…",
"snapshot_name": null,
"model_count": 12,
"deep_link": "https://istari.example.com/systems/dd603c8e-…/snapshots/5f0c3a4e-…"
}
| Field | Description |
|---|---|
snapshot_id | The new commit's ID |
snapshot_name | The commit's name if it has one, otherwise null |
model_count | Number of Resources tracked on the branch at this commit |
deep_link | A URL into the web app for this commit |
system.change_request.opened
"data": {
"change_request_id": "f8be792e-…",
"title": "Wing span 3.20 m → 3.26 m",
"description": "Updated geometry and loads cases.",
"source_tag": { "id": "d6d3a70b-…", "name": "wing-span-update" },
"target_tag": { "id": "502ef414-…", "name": "baseline" },
"deep_link": "https://istari.example.com/systems/dd603c8e-…/change-requests/f8be792e-…"
}
| Field | Description |
|---|---|
change_request_id | The change request's ID |
title | Its title, or null |
description | Its description, or null |
source_tag | The branch being merged: id and name |
target_tag | The branch it merges into: id and name |
deep_link | A URL into the web app for this change request |
Verify the signature
Always verify Istari-Signature before acting on a delivery: anyone who learns your URL can POST to it. Compute HMAC-SHA256 over the raw request body bytes, before any JSON parsing or re-serialization, with the webhook's signing secret, and compare in constant time. The signature does not cover a timestamp, so a captured request can be replayed; record each id you process and ignore repeats.
import hashlib
import hmac
def verify(body: bytes, secret: str, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, secret, signatureHeader) {
const expected = Buffer.from("sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"));
const received = Buffer.from(signatureHeader);
return expected.length === received.length && timingSafeEqual(expected, received);
}
The signing secret (whsec_…) is shown once, when the webhook is created, and cannot be retrieved afterwards. It cannot be rotated: to change it, delete the webhook and create a new one.
GitLab Pipeline targets
For a GitLab Pipeline webhook (gitlab_pipeline in the API) the platform calls GitLab's pipeline trigger API:
POST {gitlab_url}/api/v4/projects/{project}/trigger/pipeline
The request is form-encoded and carries the stored trigger token, the configured ref, and three pipeline variables:
| CI/CD variable | Value |
|---|---|
ISTARI_EVENT_TYPE | The event type, for example system.change_request.opened |
ISTARI_DELIVERY_ID | The delivery ID |
ISTARI_PAYLOAD | The full JSON payload described above, as a string |
There is no HMAC signature for GitLab targets; the trigger token authenticates the request. GitLab answers 201 when the pipeline is created (recorded as Delivered), 404 for an invalid token or project, and 400 for an unknown ref.
The trigger runs the .gitlab-ci.yml on that ref, and any job without rules runs on every event. Use rules on CI_PIPELINE_SOURCE and ISTARI_EVENT_TYPE to choose which jobs run for which event:
review-change-request:
image: python:3.12-alpine
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" && $ISTARI_EVENT_TYPE == "system.change_request.opened"
script:
- echo "$ISTARI_PAYLOAD" | python -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(d["title"], d["source_tag"]["name"], "->", d["target_tag"]["name"])'
rebuild-on-commit:
image: python:3.12-alpine
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" && $ISTARI_EVENT_TYPE == "system.snapshot.created"
script:
- echo "$ISTARI_PAYLOAD" | python -c 'import json,sys; p=json.load(sys.stdin); print("commit", p["data"]["snapshot_id"], "on", p["system"]["name"])'
Delivery and retries
- Deliveries are asynchronous. Events are queued and attempted by a background process, typically within 30 seconds of the event.
- Each attempt times out after 10 seconds. A
2xxresponse marks the delivery delivered. - A failed attempt is retried after 30 s, 2 min, 10 min, and 1 h. After the fifth failure the delivery is dead-lettered and not attempted again.
- A
410 Goneresponse disables the webhook. No further events are delivered until an Editor re-enables it. - Only enabled webhooks receive deliveries. Events that occur while a webhook is disabled, or before it was created, are not delivered later.
- Delivery history (the 50 most recent deliveries per webhook) is retained for 30 days. Deleting a webhook deletes its history.
Constraints
- Up to 10 webhooks per System.
- Target URLs, including the GitLab base URL, must be
https://and must resolve to a publicly routable IP address. Private (RFC 1918), loopback, link-local, and similar ranges are rejected when the webhook is saved and again at connection time. - Redirects are not followed.
- The target type is immutable. For
gitlab_pipeline, changinggitlab_urlorprojectrequires supplyingcredentialagain in the same request. - Credentials and signing secrets are encrypted at rest. Responses expose at most the last four characters of a credential (
credential_masked).
REST API
Webhooks are managed under /api/v2/systems/{system_id}/webhooks with a bearer token: a Key or a Personal Access Token. Reading requires Viewer; creating, updating, and deleting require Editor on the System.
| Method | Path | Purpose |
|---|---|---|
POST | /api/v2/systems/{system_id}/webhooks | Create a webhook |
GET | /api/v2/systems/{system_id}/webhooks | List webhooks (paginated: page, size) |
GET | /api/v2/systems/{system_id}/webhooks/{webhook_id} | Get a webhook |
PATCH | /api/v2/systems/{system_id}/webhooks/{webhook_id} | Update a webhook |
DELETE | /api/v2/systems/{system_id}/webhooks/{webhook_id} | Delete a webhook |
GET | /api/v2/systems/{system_id}/webhooks/{webhook_id}/deliveries | The 50 most recent deliveries |
The routes return 404 when webhooks are not enabled on the deployment.
Create
curl -X POST "$ISTARI_REGISTRY_URL/api/v2/systems/$SYSTEM_ID/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Loads re-analysis",
"target_type": "http",
"target_config": {"url": "https://hooks.example.com/istari"},
"event_types": ["system.snapshot.created", "system.change_request.opened"]
}'
For a GitLab target:
{
"name": "Partner CI",
"target_type": "gitlab_pipeline",
"target_config": { "gitlab_url": "https://gitlab.example.com", "project": "group/project", "ref": "main" },
"event_types": ["system.change_request.opened"],
"credential": "<pipeline trigger token>"
}
The response is the webhook. signing_secret is populated only in this response, and only for http targets (null for gitlab_pipeline). credential_masked holds the last four characters of the credential when one was supplied.
Update
PATCH accepts any subset of name, target_config, event_types, credential, and enabled. Omit credential to leave it unchanged; send "credential": null to remove it (HTTP targets only).
curl -X PATCH "$ISTARI_REGISTRY_URL/api/v2/systems/$SYSTEM_ID/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Deliveries
curl "$ISTARI_REGISTRY_URL/api/v2/systems/$SYSTEM_ID/webhooks/$WEBHOOK_ID/deliveries" \
-H "Authorization: Bearer $TOKEN"
Each item has id, webhook_id, event_type, status (pending, delivered, failed, dead_letter, or disabled_410), attempt_count, response_status, response_message (the first 1024 characters of the response body), created, last_attempt_at, and next_attempt_at.
Python client
istari-digital-client exposes the same operations on Client under their generated names, together with the NewSystemWebhook, UpdateSystemWebhook, SystemWebhookEvent, WebhookTargetType, TargetConfig, HttpTargetConfig, and GitlabPipelineTargetConfig models. See SDK setup for creating the Client.
from istari_digital_client import (
HttpTargetConfig,
NewSystemWebhook,
SystemWebhookEvent,
TargetConfig,
WebhookTargetType,
)
webhook = client.create_webhook_api_v2_systems_system_id_webhooks_post(
system_id,
NewSystemWebhook(
name="Loads re-analysis",
target_type=WebhookTargetType.HTTP,
target_config=TargetConfig(HttpTargetConfig(url="https://hooks.example.com/istari")),
event_types=[SystemWebhookEvent.SYSTEM_DOT_SNAPSHOT_DOT_CREATED],
),
)
print(webhook.signing_secret) # only present in the create response
deliveries = client.list_webhook_deliveries_api_v2_systems_system_id_webhooks_webhook_id_deliveries_get(
system_id, webhook.id
)