tatersecurity.com Open App

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.

Audience: developers building or maintaining a receiver for TATER Ops Event Webhooks - the machine-facing counterpart to Task Notifications. If you just need to configure a subscription in the UI, use the guide linked above instead.

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": { }
}
FieldTypeNotes
eventstringEvent type, e.g. task.created. Matches the X-TATER-Event-Type header - branch on either.
timestampstring (ISO 8601)When this delivery attempt was built. A resend re-signs the ORIGINAL stored payload rather than regenerating this field - see Delivery reliability.
tenantIdstringThe Azure AD tenant id owning this event. Not a secret, but treat as an identifier, not a credential.
organizationIdstringThe TATER organization id. Every subscription is scoped to one organization; you should never receive an event for any other organizationId.
subjectType / subjectIdstringThe 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.
actorEmailstring (optional)The user who triggered the event, when known. Absent for system/automated actions.
dataobjectEvent-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"
}
FieldNotes
taskIdTreat 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.
viaThe creation channel: web, mcp, agent, api, email-intake, self-service, or similar. This is what the loop guard filters on - see below.
category / priorityMatch the org's configured Tasker category/priority taxonomy. Also what a subscription's optional filters match against.
assignedToEmailThe 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 / externalRefsOptional. Present only when the task is linked to another TATER entity (risk, change request, vendor, etc.) or has external ticket references.
urlA deep link directly into the task in TATER Ops.
Additional event types beyond 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/json
  • X-TATER-Event-Type - the event type string, identical to the body's event field. 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
}
Common mistake: reading the request body with a framework that auto-parses JSON before your handler runs (many web frameworks do this by default) discards the raw bytes you need to sign. Configure your framework to give you the raw body - or capture it in raw form yourself before the JSON parser touches it - or every signature check will fail even for legitimate deliveries.

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).

Do not rely on this as your only safeguard. Apply your own 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:

AttemptDelay before this attempt
1 (initial)immediate
21 minute
35 minutes
415 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.

TATER only knows whether your endpoint returned a 2xx. It has no visibility into what your automation does after that. If your handler has a bug, TATER will not retry on your behalf - you already acknowledged the delivery. Design your handler to be idempotent (safe to process the same 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:

Run one instance per client tenant, using that tenant's own org-scoped API key and webhook secret. There is no TATER-hosted, shared, multi-tenant version of this runner - see the README in the linked folder for why.