Ops Event Webhooks - Developer Reference
The exact wire format for TATER's signed outbound event webhooks: the envelope schema, the task.created payload fields, how to verify X-TATER-Signature, the MCP loop guard, and how delivery retry/dead-letter works. For the in-app setup walkthrough, see Ops Event Webhooks - Work New Tickets Automatically.
Overview
A TATER organization can create one or more Ops Event Webhook subscriptions (Manage → Connections → Ops Event Webhooks). Each subscription has its own endpoint URL, secret, event-type selection, and optional category/priority/via filters. When a matching event occurs, TATER POSTs a signed JSON body to every active, matching subscription for that event type.
This is a distinct mechanism from the single per-org SIEM/webhook integration documented in Power Automate Integration (which only fires scan.completed). Ops Event Webhooks supports multiple named subscriptions per org, a wider event catalog anchored on task.created, per-subscription filters, a delivery log, and automatic retry with dead-lettering. Both use the identical signature scheme described below, so a single verifier function can validate either.
Event envelope
The request body is JSON with this top-level shape for every event type:
{
"event": "task.created",
"timestamp": "2026-07-03T14:32:00.000Z",
"tenantId": "d9a7e925-...",
"organizationId": "org-xxxxxxxx",
"subjectType": "tasker-task",
"subjectId": "task-uuid",
"actorEmail": "jane@example.com",
"data": { }
}
| Field | Type | Notes |
|---|---|---|
event | string | Event type, e.g. task.created. Matches the X-TATER-Event-Type header - branch on either. |
timestamp | string (ISO 8601) | When this delivery attempt was built. A resend re-signs the ORIGINAL stored payload rather than regenerating this field - see Delivery reliability. |
tenantId | string | The Azure AD tenant id owning this event. Not a secret, but treat as an identifier, not a credential. |
organizationId | string | The TATER organization id. Every subscription is scoped to one organization; you should never receive an event for any other organizationId. |
subjectType / subjectId | string | The entity type/id the event is about, e.g. tasker-task / task-uuid. Redundant with data.taskId for task events - use whichever is more convenient. |
actorEmail | string (optional) | The user who triggered the event, when known. Absent for system/automated actions. |
data | object | Event-specific fields. See task.created below. |
task.created payload
data for a task.created event:
{
"taskId": "task-uuid",
"title": "OneDrive sync stuck",
"description": "User reports OneDrive icon shows a red X ...",
"category": "IT",
"priority": "High",
"status": "Open",
"source": "web",
"via": "web",
"requesterEmail": "user@example.com",
"assignedToEmail": "helpdesk@example.com",
"createdBy": "user@example.com",
"dueDate": "2026-07-07",
"linkedEntity": { "type": "risk", "id": "risk-uuid" },
"externalRefs": [ { "system": "jira", "id": "OPS-4821", "url": "https://..." } ],
"url": "https://ops.tatersecurity.com/?page=task-detail&id=task-uuid"
}
| Field | Notes |
|---|---|
taskId | Treat as the key. Use it to fetch full task detail via GET /tasker/tasks/{taskId} or the get_tasker_task MCP tool, and to write progress back as a comment. |
via | The creation channel: web, mcp, agent, api, email-intake, self-service, or similar. This is what the loop guard filters on - see below. |
category / priority | Match the org's configured Tasker category/priority taxonomy. Also what a subscription's optional filters match against. |
assignedToEmail | The current assignee's email address. Field name matches the existing Automations event-field catalog (Manage → Content & Automation → Automations) so a condition can reference the same field across both features. May be absent if unassigned. |
linkedEntity / externalRefs | Optional. Present only when the task is linked to another TATER entity (risk, change request, vendor, etc.) or has external ticket references. |
url | A deep link directly into the task in TATER Ops. |
task.created may be added to a subscription's event list from the same catalog TATER's Automations feature uses. Every event shares the same envelope; only the shape of data differs per event type. Don't assume the field set above is exhaustive for other event types.
The catalog also includes the compliance and monitoring event families that were previously forwarded to SIEM only. Subscribable types now include:
control.passed / control.failed, scan.completed,
itdr.alert.created, monitoring.finding.created / .recurred,
risk_acceptance.expiring / .expired, license.exceeded,
onedrive.health.degraded / .recovered,
power_automate.flow.degraded / .recovered, and
remediation.triggered / .completed / .failed.
Each carries the same signed envelope; the data field keys mirror the corresponding SIEM event payload. Repeating degraded-state events are day-bucketed per device or flow, so an Automation fires at most once per day per subject while the underlying condition persists.
Request headers
Content-Type: application/jsonX-TATER-Event-Type- the event type string, identical to the body'seventfield. Use this to route without parsing the body first.X-TATER-Signature: sha256=<hex-hmac>- HMAC-SHA256 of the exact raw request body, keyed with your subscription's secret. See below.User-Agent: TATER-Webhooks/1.0
Verifying the signature
Compute the HMAC over the raw bytes of the request body exactly as received, before any JSON parsing. Re-serializing the parsed object and hashing that will not reliably reproduce the original bytes (key order, whitespace, and number formatting are not guaranteed to round-trip), and will cause false rejections. Compare the result to the header in constant time - a plain string or byte-array ==/=== comparison leaks timing information proportional to how many leading bytes match, which is a real (if narrow) side channel against your secret.
Python
import hmac, hashlib
def verify(raw_body: bytes, header_value: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header_value)
Node.js
const crypto = require('crypto');
function verify(rawBody, headerValue, secret) {
if (!headerValue || !headerValue.startsWith('sha256=')) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(headerValue);
if (a.length !== b.length) return false; // timingSafeEqual requires equal length
return crypto.timingSafeEqual(a, b);
}
PowerShell
function Test-TaterSignature {
param([string]$RawBody, [string]$HeaderValue, [string]$Secret)
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($Secret))
$hash = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($RawBody))
$expected = 'sha256=' + [BitConverter]::ToString($hash).Replace('-', '').ToLower()
# Fixed-time compare: hash both sides again rather than short-circuiting a byte loop.
$a = [Text.Encoding]::UTF8.GetBytes($expected)
$b = [Text.Encoding]::UTF8.GetBytes($HeaderValue)
if ($a.Length -ne $b.Length) { return $false }
$diff = 0
for ($i = 0; $i -lt $a.Length; $i++) { $diff = $diff -bor ($a[$i] -bxor $b[$i]) }
return $diff -eq 0
}
The MCP loop guard
Every task creation channel fires task.created, including tasks your own automation creates through the TATER API or MCP. If your receiver creates or updates tickets, this can re-trigger itself in a loop.
Each subscription has an excludeVia list. When you fetch a subscription, TATER reports the effective list - if you never set it, or set it to an unrecognized value, it defaults to ["mcp"]. An explicit empty array is required to receive MCP-created events. This filter is evaluated before category/priority filters, and matches case-insensitively against the event's top-level via field (falling back to data.via for older event shapes).
via check inside your receiver too (defense in depth) - see the reference runner linked below, which does exactly this independently of the TATER-side filter.
Delivery reliability
Every delivery attempt (success or failure) is recorded to the subscription's Delivery Log, visible in Manage → Connections → Ops Event Webhooks. A delivery is considered successful only on an HTTP 2xx response; anything else (4xx, 5xx, timeout, connection refused, DNS failure) is treated as failed and retried automatically:
| Attempt | Delay before this attempt |
|---|---|
| 1 (initial) | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 (final) | 1 hour |
After the final attempt still fails, the delivery is marked dead-letter and stops retrying automatically. Any delivery - including a dead-lettered one - can be manually resent from the Delivery Log. A resend replays the exact original payload string verbatim (same timestamp, same content), re-signed with the subscription's current secret, so a resend after a secret rotation still verifies correctly on your end.
taskId/event more than once) and handle your own downstream failures.
Reference implementation
A minimal, dependency-free reference receiver - covering signature verification, the defense-in-depth via filter, and an example round trip (fetch the task, post a progress comment) - is available for download, with both a bare Node.js and an Azure Functions variant:
- Ops Event Webhook reference runner (also linked from the Runbooks/Agent downloads area)
Related guides
- Ops Event Webhooks - Work New Tickets Automatically - in-app setup walkthrough
- API Reference - authenticating your receiver's callback into the TATER API (fetching task detail, posting comments)
- Swagger UI / openapi.json - the full machine-readable endpoint reference for callback development
- Power Automate Integration - the separate, single-event
scan.completedSIEM webhook - MCP Server Setup - what
via: "mcp"means and why it's excluded by default
TATER