Webhooks
Webhooks deliver App-qualified events to an HTTPS endpoint bound to an entitled API client. Each request is signed over the exact payload bytes and a timestamp.
Webhook endpoint and secret administration is currently operator-managed in Vision. A public registration API and certified event catalog are not yet part of the preview OpenAPI contract.
Endpoint requirements
Endpoints must use HTTPS, match the environment allowlist, and resolve only to permitted public addresses. Loopback, private, link-local, reserved, and metadata-service destinations are rejected at registration and delivery.
Verify a delivery
Each delivery is a POST with a JSON body (schemaVersion: "vision-webhook-event.v1") and three headers:
- Name
vision-webhook-id- Description
Unique delivery ID (
whd_...). Use it to drop replays.
- Name
vision-webhook-timestamp- Description
Unix seconds when the delivery was signed.
- Name
vision-webhook-signature- Description
v1=followed by the hex HMAC-SHA256 of{timestamp}.{deliveryId}.{rawBody}with your endpoint secret.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyVisionWebhook(
headers: Headers,
rawBody: string,
secret: string,
seen: (id: string) => boolean,
) {
const id = headers.get('vision-webhook-id') ?? ''
const timestamp = Number(headers.get('vision-webhook-timestamp'))
const signature = headers.get('vision-webhook-signature') ?? ''
if (
!Number.isInteger(timestamp) ||
Math.abs(Date.now() / 1000 - timestamp) > 300
)
throw new Error('stale timestamp')
if (seen(id)) throw new Error('replayed delivery')
const expected =
'v1=' +
createHmac('sha256', secret)
.update(`${timestamp}.${id}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature)
if (a.length !== b.length || !timingSafeEqual(a, b))
throw new Error('bad signature')
}
Python
import hmac, hashlib, time
def verify_vision_webhook(headers, raw_body: bytes, secret: str, seen) -> None:
delivery_id = headers["vision-webhook-id"]
timestamp = int(headers["vision-webhook-timestamp"])
if abs(time.time() - timestamp) > 300:
raise ValueError("stale timestamp")
if seen(delivery_id):
raise ValueError("replayed delivery")
signed = f"{timestamp}.{delivery_id}.".encode() + raw_body
expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, headers["vision-webhook-signature"]):
raise ValueError("bad signature")
Verify against the raw request bytes before any JSON parsing. Reject timestamps more than five minutes away, compare in constant time, and record the delivery ID before applying side effects.
Preview status, 2026-10-28: endpoint registration, signing, retries and secret
rotation are built, but no App emits events yet, so no deliveries are sent.
Until an event catalogue is published, use Directory's change feed (GET /api/v1/changes) to follow changes.
Retries and idempotency
Return a 2xx response only after durable acceptance. Retryable failures use bounded exponential delay. Permanent policy failures stop immediately, and exhausted deliveries remain visible to administrators.
Secret lifecycle
Webhook secrets are shown once, encrypted at rest with AES-256-GCM, and independently rotatable. During a bounded overlap, verify against the active and overlap secret versions. Never log the secret or complete signature header.