push connector · cloudevents + hmac

Webhooks

Push Wytness anomaly alerts anywhere. CloudEvents 1.0 body. HMAC-SHA256 signature header. Retries with jitter. Public spec.

WYTNESSYOUR STACKLedgersigned eventhash-chainedConnectorbuilds the payloadretries with backoffYour receiverHTTPS endpointauth: HMAC signaturePOSTdelivery log · status, HTTP, attemptsretries exhausted → recorded as failed, never silentSignature travels with itWytness-Signature (HMAC-SHA256) travels with the body — re-verifiable at your receiver

what it does

Where your alerts land

The generic webhook connector pushes Wytness anomaly alerts (severity, rule, agent ID, description) to any HTTPS endpoint you control — no raw event payloads, tool parameters, or PII. The envelope is CloudEvents 1.0. Every delivery is signed HMAC-SHA256 on a timestamp-prefixed body to defeat replay. A delivery that exhausts its retries is recorded as failed in the delivery log — never silently dropped.

setup

From nothing to first delivery

01

In the Wytness app, go to Integrations → Connectors → Add connection → Generic webhook and paste your receiver URL.

02

Choose an HMAC secret and enter it in the wizard; store the same value in your receiver's environment as WYTNESS_WEBHOOK_SECRET. Wytness keeps it encrypted and never displays it again.

03

Optionally narrow deliveries with severity or rule-name filters (e.g. only `high` and `critical` alerts).

04

On your server, verify the `Wytness-Signature` header on incoming POST requests before parsing the body. Reject if the timestamp is older than 5 minutes.

05

Click Test connection. A signed CloudEvent should hit your endpoint within seconds.

the payload

What arrives at your endpoint

One CloudEvents 1.0 envelope per anomaly. The signature covers the raw body bytes prefixed with the timestamp, so verify against exactly what you received — not a re-serialised parse.

delivery.json
// Headers on the POST:
// Content-Type: application/cloudevents+json
// Wytness-Signature: t=1716470621,v1=8af0c6c1c1b5e2c0e1d2f3a4b5c6d7e8f9a0b1c2d3e4f5061728394a5b6c7d8e
// Wytness-Delivery: d7f3e2c1-9a4b-4d6e-8f1a-2b3c4d5e6f70
// Wytness-Event: ai.wytness.anomaly.detected.v1
//
// Body:
{
"specversion": "1.0",
"type": "ai.wytness.anomaly.detected.v1",
"source": "https://api.wytness.ai/orgs/7c2a91d5-4b3e-4f8a-9c1d-2e3f4a5b6c7d",
"id": "9f2d1c34-8a7b-4e2d-b1a0-5c6d7e8f9a0b",
"time": "2026-05-23T14:23:41Z",
"datacontenttype": "application/json",
"data": {
"alert_id": "9f2d1c34-8a7b-4e2d-b1a0-5c6d7e8f9a0b",
"agent_id": "payments-reconciler-v3",
"rule_name": "off_hours_activity",
"severity": "high",
"description": "Tool invocation outside business hours window.",
"detected_at": "2026-05-23T14:23:41.182Z",
"url": "https://app.wytness.ai/anomalies?alert_id=9f2d1c34-8a7b-4e2d-b1a0-5c6d7e8f9a0b"
}
}

prove it landed

Verify end to end

The same HMAC pattern works in any language with an HMAC-SHA256 primitive — this is the whole receiver-side contract:

verify.py
# Drop into your webhook receiver. Reject any POST whose timestamp is
# more than 5 minutes old or whose signature doesn't match.
import hmac, hashlib, time
WYTNESS_WEBHOOK_SECRET = "<the-secret-you-chose-in-the-wizard>"
def verify(body: bytes, signature_header: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > 300: # 5-minute replay window
return False
signed = f"{t}.".encode() + body
expected = hmac.new(
WYTNESS_WEBHOOK_SECRET.encode(),
signed,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])

the boundary

What flows and what doesn't

Sends

CloudEvents 1.0 JSON envelope via HTTP POST

Headers

`Wytness-Signature: t=<unix>,v1=<hmac-sha256(t.body)>`, `Wytness-Delivery: <uuid>`, `Wytness-Event: <type>`

Retries

60s → 300s → 900s with ±25% jitter; 401/403/404/410/422 stop retries immediately

Reads

Nothing. One-way push from Wytness to your endpoint.

Network

One inbound HTTPS rule: the internet must reach your receiver. Wytness source IPs are not fixed — authenticate with the Wytness-Signature HMAC, not an IP allowlist.

stated plainly

Limitations

One event type today

ai.wytness.anomaly.detected.v1 is the only type the dispatcher sends — connectors forward anomaly alerts, not the raw event stream. The Wytness-Event header names the type so your receiver can branch when more arrive.

Public endpoints only

Private and internal network addresses are refused by design (SSRF protection). Your receiver needs a public HTTPS endpoint; if its certificate is from an internal CA, switch off Verify TLS certificate on the connection.

Replay tolerance is yours to enforce

Wytness signs a timestamped body; the 5-minute rejection window runs in your receiver. The Wytness-Delivery UUID is unique per delivery attempt — dedupe on it if you need exactly-once processing.

round trip

Smoke-test your receiver

Anything other than a fast 2xx is retried on the ladder above; the terminal statuses (401/403/404/410/422) fail the delivery immediately.

receiver_test.sh
# Fire a test delivery from the wizard's Test connection button (or curl
# with the secret you chose). Your receiver should respond 2xx quickly.
$ curl -i https://your-receiver.example.com/wytness \
-H "Content-Type: application/cloudevents+json" \
-H "Wytness-Signature: t=$(date +%s),v1=<computed>" \
-H "Wytness-Delivery: $(uuidgen)" \
-H "Wytness-Event: ai.wytness.anomaly.detected.v1" \
-d @sample_event.json

related

Keep reading

/docs/connectors

Connector setup guide

The full integrator contract — network requirements, delivery semantics, and every wizard field. Read the guide.

/connectors

All connectors

Sentinel, Splunk, ServiceNow, Slack, and the generic webhook. See the index.

tier

Tier availability

Business plan or higher.

Your endpoint. Your language. A signed, public spec.

We set no cookies. Sign-in and preferences use essential first-party browser storage only — no tracking, advertising, or third-party analytics. Privacy Policy