Webhooks
Workshop can POST events to an HTTPS endpoint of your choosing as they happen, letting you forward Workshop activity into your own systems — SIEMs, ticketing, chat, or custom automation. Each delivery is signed following the Standard Webhooks specification so your receiver can verify it genuinely came from Workshop.
Event sources
There are three independent webhook sources. Each has its own destination URL, signing secret, and delivery filters, so you can send different event types to different endpoints (or the same one).
| Source | Fires when | Filter |
|---|---|---|
| Audit events | Any change is made to Workshop (rules, settings, tags, etc.) | Event type |
| Signal reports | A detection signal report is first received, and again on each triage state change | Report state |
| Software approvals | A piece of software is approved through an approval workflow for the first time | None |
Audit events
Fires once for every audit event — the same record of every change made to Workshop that appears in the audit log, whether the change was made through the UI or the API. See the Audit documentation for the full list of audit event types.
By default every audit event is delivered. You can narrow delivery to specific event types; leave the filter empty to deliver all of them.
Signal reports
Fires when a detection signal report is first received from a host (state NEW) and again each time a report's triage state changes (for example when it moves to ACKNOWLEDGED or REMEDIATED).
The state filter applies to both cases — receipt counts as the NEW state. Leave it empty to deliver for every state. The available states are NEW, ACKNOWLEDGED, INVESTIGATING, REMEDIATED, and DISMISSED.
Software approvals
Fires once, the first time a given piece of software (a binary or a bundle) is approved through any approval workflow — self-service, designated approver, or social voting. Subsequent approvals of the same software do not fire. See the Approval Workflows documentation for how approvals work.
This source has no filter. The payload's requesting_user field identifies who requested the software for self-service and designated-approver workflows; it is empty for social voting, which has no single requester.
Configuring webhooks
Navigate to Settings → Webhooks. Each source is configured in its own section with the following fields:
- Enable toggle — turns delivery on or off. Disabling a source stops delivery but keeps its URL and secret so you can re-enable it later without re-entering them.
- Destination URL — the HTTPS endpoint that receives deliveries.
- Signing secret — the key used to sign every delivery (see Verifying signatures). It must be at least 24 bytes.
- Filter — event types (audit events) or states (signal reports), where applicable.
- Custom headers (audit events) — additional HTTP headers sent with every delivery, e.g. an
Authorizationheader your receiver expects.
Click Save Changes to apply. Saving requires the write:settings permission.
The signing secret is write-only. Workshop never displays or returns it again after you save it. When editing an existing configuration, leave the secret field blank to keep the current secret — only enter a value when you want to replace it. A source cannot be enabled without a secret, since deliveries can't be signed without one.
Delivery format
Each delivery is an HTTP POST with a Content-Type of application/json. The body is a JSON object with exactly one field set, identifying the source:
| Field | Source |
|---|---|
audit_event | Audit events |
signal_report | Signal reports |
software_approval | Software approvals |
Payloads are serialized from Workshop's protobuf definitions, so:
- Field names are
snake_caseand enum values are their string names (not numbers). - Every field is emitted even when empty — an unset string is
"", a number is0, a boolean isfalse, an unset nested object isnull, and an empty list is[]. Don't assume a missing field; assume an empty one.
The examples below are trimmed to the fields worth highlighting; a real delivery includes the remaining fields at their empty values as described above.
- Audit event
- Signal report
- Software approval
{
"audit_event": {
"id": "0f9c2e6a-1d3b-4a7e-9c2f-8b1a5e6d7c40",
"transaction_id": "3a1b8f22-6c4d-4e19-8f2a-1b7c9d0e5a63",
"timestamp": "2026-07-20T15:04:05Z",
"actor": "user:rah@northpole.security",
"event": "AUDIT_EVENT_RULE_UPSERT",
"resource": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"outcome": "OUTCOME_SUCCESS",
"details": "{\"policy\":\"ALLOWLIST\",\"rule_type\":\"BINARY\"}",
"previous_value": "",
"ai_chat_conversation_id": "",
"via_mcp": false
}
}
The host object carries the full set of host fields; only a few are shown here (see the Hosts documentation).
{
"signal_report": {
"id": "6d2f1c88-9a7e-4b31-8c05-2e9f4a1b7d63",
"host": {
"uuid": "A14A8806-5878-45A2-81E4-DAB36020B560",
"serial": "C02XL0ZYJGH5",
"hostname": "workstation.example.com",
"os_version": "15.5",
"primary_user": "jane@example.com",
"os_type": "OS_TYPE_MACOS"
},
"name": "suspicious_persistence",
"severity": "SEVERITY_CRITICAL",
"description": "A launch agent was written by an unsigned binary.",
"event_ids": ["execution:8f0a1b2c-3d4e-5f60-7182-93a4b5c6d7e8"],
"reported_at": "2026-07-20T15:04:05Z",
"processed_time": null,
"state": "SIGNAL_REPORT_STATE_NEW",
"assignee": "",
"resolved_by": "",
"labels": ["persistence", "unsigned"],
"os_type": "OS_TYPE_MACOS"
}
}
For a bundle approval, the binary field is replaced by a bundle object that nests its constituent binaries.
{
"software_approval": {
"blockable_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"app_name": "Example.app",
"approving_user": "approver@example.com",
"requesting_user": "requester@example.com",
"binary": {
"sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"cdhash": "",
"signing_id": "com.example.app",
"team_id": "EQHXZ8M8AV",
"file_name": "Example",
"signed_by": [
{
"sha256": "b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9",
"common_name": "Developer ID Application: Example Inc (EQHXZ8M8AV)",
"organization": "Example Inc",
"organizational_unit": "",
"valid_from": null,
"valid_until": null,
"signed_by": "",
"first_seen": null
}
],
"entitlements": [],
"first_seen": null,
"malicious_state": "MALICIOUS_STATE_BENIGN",
"bundle_relative_path": "",
"signing_timestamp": null,
"secure_signing_timestamp": null,
"signing_status": "SIGNING_STATUS_PRODUCTION"
}
}
}
Request headers
Every delivery includes the Standard Webhooks headers plus a Workshop User-Agent:
| Header | Description |
|---|---|
webhook-id | Unique identifier for the delivery (see below). Use it to deduplicate. |
webhook-timestamp | Delivery time as a Unix timestamp in seconds. |
webhook-signature | The signature over the payload (see Verifying signatures). |
Content-Type | Always application/json. |
User-Agent | Workshop/<version> (+https://northpole.security) |
Any custom headers you configure are added on top of these.
The webhook-id is namespaced by source so you can tell deliveries apart:
| Source | webhook-id format |
|---|---|
| Audit events | audit:<event-id> |
| Signal reports | signal:<uuid> |
| Software approvals | software-approval:<uuid> |
The same webhook-id is reused across retries of a delivery, so it's safe to deduplicate on it.
Verifying signatures
Workshop signs every delivery using the Standard Webhooks scheme. The signature is an HMAC-SHA256 over the string {webhook-id}.{webhook-timestamp}.{body}, base64-encoded, and sent in the webhook-signature header as v1,<signature>.
The simplest way to verify is with one of the Standard Webhooks libraries, which handle the signature construction and comparison for you — construct a verifier with your configured secret and pass it the raw request body and headers. Note that the standard verification also enforces a 5-minute timestamp tolerance to guard against replay, so your receiver's clock should be reasonably in sync.
The signing secret is the HMAC key. Workshop follows the Standard Webhooks convention: if you enter a base64-encoded value, its decoded bytes are used as the key; if you enter a plain string, the string's raw bytes are used. Standard Webhooks verification libraries expect a base64-encoded secret, so the simplest setup is to generate a random secret, base64-encode it, and use that same string both in Workshop and in your receiver.
Delivery behavior
Deliveries happen asynchronously, off the request path — a webhook failure never blocks or fails the underlying action (for example, an audited change still succeeds even if its webhook can't be delivered). Delivery is best-effort: failures are logged server-side, but there is no delivery dashboard or manual re-drive.
- Retries — after the initial attempt, a failed delivery is retried up to 5 more times (six attempts in total) with exponential backoff (starting at 100ms, capped at 30s). Retries happen on connection errors, HTTP 429, and 5xx responses.
- Success — any 2xx response is treated as success. Redirects are not followed.
- Timeouts — each attempt has its own 30s timeout, and the whole delivery — loading settings, building the client, and all attempts with their backoffs — is bounded to 60s.
- Payload size — deliveries are capped at 1MB. A payload larger than that is dropped rather than sent.
Your endpoint should acknowledge quickly with a 2xx and do any heavy processing asynchronously.
Security
- Destination URLs must use HTTPS (plain HTTP is only permitted for
localhost, for local testing). - URLs may not contain embedded credentials (
user:pass@host). - URLs that resolve to private, internal, or link-local addresses are rejected to guard against SSRF. Redirects are never followed.
- The signing secret is write-only: it is never returned by the API or shown in the UI, and it is stripped from audit log entries.
Configuring via the API
Webhook settings can also be managed through the API:
GetWebhookSettingsretrieves the current configuration for every source. Requires theread:settingspermission. The write-only signing secret is never included in the response.UpdateWebhookSettingsreplaces the configuration for every source. Requires thewrite:settingspermission.
Because UpdateWebhookSettings replaces the entire configuration, include every source you want to keep in each call — omitting a source clears it. Sending an empty secret for a source keeps its existing secret rather than clearing it.