Skip to main content

Signed webhooks

Signed webhooks

Last verified: 2026-06-16.

The signed webhook channel delivers a JSON event to a URL you control whenever a covered event occurs — for streaming sign-in events into a SIEM or automation. This is a paid feature.

Configure an endpoint

Under Awthy Security → Notifications (administrators only):

  1. Add a destination URL (HTTPS).
  2. Provide a signing secret. The secret is encrypted at rest and is write-only — it is never shown again after you save it.
  3. Use Send test to deliver a signed test event and confirm your endpoint verifies it.

Request format

Each delivery is an HTTP POST with a JSON body and these headers:

  • Awthy-Webhook-Id — a stable idempotency id. The same id is reused across retries of the same event, so your receiver can deduplicate.
  • Awthy-Signaturet=<unix-timestamp>,v1=<hex-hmac>.

The signature is HMAC-SHA256 over the exact string timestamp.id.raw_body, using your signing secret. Verify against the raw request body bytes — do not re-serialize the JSON first, or the signature will not match.

Verifying a request (Node.js)

import crypto from 'node:crypto';

function verify(req, rawBody, secret) {
const sigHeader = req.headers['awthy-signature'] ?? '';
const id = req.headers['awthy-webhook-id'] ?? '';
const parts = Object.fromEntries(sigHeader.split(',').map((p) => p.split('=')));
const timestamp = parts.t;
const presented = parts.v1;

// Reject stale deliveries (replay protection): ~5 minute tolerance.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${id}.${rawBody}`)
.digest('hex');

return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(presented));
}

Use a constant-time comparison (timingSafeEqual) so verification does not leak the secret through timing.

Retry behavior

If your endpoint does not return a 2xx status, Awthy retries with exponential backoff, up to three attempts. Each attempt is signed with a fresh timestamp, but the request body and Awthy-Webhook-Id stay the same — so deduplicate on the webhook id, not the timestamp.