Skip to main content
Version: 2026.10

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 typeFires whenIdempotency key (id)
system.snapshot.createdA commit (snapshot) is recorded on any branch of the Systemevt_<commit id>
system.change_request.openedA 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:

HeaderValue
Content-Typeapplication/json
Istari-EventThe event type, for example system.snapshot.created
Istari-DeliveryThe delivery ID. Retries of the same delivery reuse it
Istari-Signaturesha256=<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": {}
}
FieldDescription
idStable idempotency key for the event
event_typeOne of the event types above
occurred_atWhen the event occurred, ISO 8601, UTC
istari_instanceThe web app URL of the platform that sent the event, as configured by your IT administrator
systemThe System the event belongs to: id, name, and the owning organization's tenant_id
actorThe user whose action caused the event: user_id and email (email may be null)
dataEvent-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-…"
}
FieldDescription
snapshot_idThe new commit's ID
snapshot_nameThe commit's name if it has one, otherwise null
model_countNumber of Resources tracked on the branch at this commit
deep_linkA 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-…"
}
FieldDescription
change_request_idThe change request's ID
titleIts title, or null
descriptionIts description, or null
source_tagThe branch being merged: id and name
target_tagThe branch it merges into: id and name
deep_linkA 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 variableValue
ISTARI_EVENT_TYPEThe event type, for example system.change_request.opened
ISTARI_DELIVERY_IDThe delivery ID
ISTARI_PAYLOADThe 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:

.gitlab-ci.yml
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 2xx response 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 Gone response 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, changing gitlab_url or project requires supplying credential again 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.

MethodPathPurpose
POST/api/v2/systems/{system_id}/webhooksCreate a webhook
GET/api/v2/systems/{system_id}/webhooksList 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}/deliveriesThe 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
)