Dashboard

Webhooks

Webhooks let your backend get notified the moment something important happens in your Sentriq environment, instead of polling the API. Configuration (creating and managing endpoints) is a dashboard-only, account-level setting — deliveries themselves are plain signed HTTP POSTs to a URL you control.

Event types

One risk assessment can trigger several event types at once — for example, a brute-force login that gets blocked fires risk.assessment.completed, decision.block, and security.brute_force together. Each is delivered independently, once per subscribed endpoint per type.

TypeFires from
risk.assessment.completedEvery completed risk assessment.
decision.allow / .monitor / .challenge / .blockThe matching decision on that assessment.
device.newThe assessment's NEW_DEVICE signal fired.
security.credential_stuffingThe assessment's CREDENTIAL_STUFFING_SUSPECTED signal fired.
security.brute_forceThe assessment's BRUTE_FORCE_SUSPECTED signal fired.
security.automation_suspectedThe assessment's AUTOMATION_SUSPECTED signal fired.
device.trusted / device.trust_revokedA device is trusted or untrusted.
outcome.created / outcome.updatedA new or revised outcome — see Outcomes.
webhook.testPOST /v1/webhooks/{id}/test only.

Payload envelope

Every delivery shares the same envelope shape. The payload is deliberately minimal — no raw secrets, no investigation-only signal metadata, nothing beyond what's needed to act on the notification. Full detail is always a follow-up GET call away with a secret key.

{
  "id": "wh_evt_01JATG7K9N8Q2R3S4T5U6V7W8X",
  "type": "decision.challenge",
  "created_at": "2026-08-20T14:32:01Z",
  "api_version": "2026-08-01",
  "data": {
    "event_id": "evt_...",
    "event_type": "login",
    "device_id": "dev_...",
    "risk": {
      "score": 72,
      "level": "high",
      "decision": "challenge",
      "signals": [{ "code": "NEW_DEVICE", "weight": 20, "description": "..." }]
    }
  }
}

id is stable across every retry of the same logical delivery — dedupe on it (see § Replay protection below).

Signing

Every delivery is signed with HMAC-SHA256 over the timestamp and raw request body, using the webhook secret shown once when you create or rotate the endpoint. This is the same shape Stripe/GitHub-style webhook signing already uses — no custom cryptography.

Content-Type: application/json
Sentriq-Webhook-Id: wh_evt_01JATG7K9N8Q2R3S4T5U6V7W8X
Sentriq-Webhook-Timestamp: 1755700321
Sentriq-Webhook-Signature: 5f3c9a2e...  (hex HMAC-SHA256)

Sentriq-Webhook-Signature = HMAC-SHA256("{timestamp}.{raw_json_body}", your_webhook_secret).

Verification — PHP

function verifySentriqWebhook(string $rawBody, string $timestamp, string $signature, string $secret, int $toleranceSeconds = 300): bool
{
    if (abs(time() - (int) $timestamp) > $toleranceSeconds) {
        return false; // stale — see § Replay protection
    }

    $expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret);

    return hash_equals($expected, $signature); // constant-time compare
}

Verification — Node.js/TypeScript

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifySentriqWebhook(rawBody: string, timestamp: string, signature: string, secret: string, toleranceSeconds = 300): boolean {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    return false; // stale — see § Replay protection
  }

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature, 'hex');

  return a.length === b.length && timingSafeEqual(a, b); // constant-time compare
}

Both examples use a constant-time comparison (hash_equals/timingSafeEqual) deliberately — a naive ===/== string comparison leaks timing information an attacker could use to forge a valid signature byte-by-byte.

Replay protection

Verify deliveries in this order:

  1. Verify the HMAC signature (above).
  2. Check the timestamp is within a tolerance window — 5 minutes is a reasonable default; this is enforced on your side, not by Sentriq.
  3. Deduplicate on Sentriq-Webhook-Id — store recently seen IDs and ignore repeats. A retry of the same logical delivery reuses the same ID.

Delivery, retries, timeouts

  • Connect timeout: 5s. Request timeout: 10s — short on purpose so a slow endpoint never backs up delivery for everyone else.
  • Retryable: 408, 429, any 5xx, and connection/timeout/DNS failures. Permanent (no retry): most 4xx, and any 3xx — redirects are never followed.
  • 5 total attempts, with delay before each retry of 1 minute, 5 minutes, 30 minutes, then 2 hours.
  • Every attempt — success, transient failure, or permanent failure — is recorded with its HTTP status, duration, and a bounded, non-secret response snippet, visible in the dashboard delivery log.

Event ordering

Retries mean your endpoint can receive deliveries out of the order the underlying events actually happened, and — rarely — a delivery that did succeed can still trigger a retry if Sentriq's own detection of that success failed. Sentriq does not guarantee global delivery ordering. Always deduplicate on Sentriq-Webhook-Id, and use data.event_id/timestamps inside the payload for ordering — never delivery arrival order.

Security
Webhook endpoint URLs are entirely under your control, so Sentriq validates them before every delivery to make sure they can't be pointed at your own internal or private infrastructure. Only http/https URLs are accepted, and the underlying connection is pinned to the exact address that was validated for that request.

Managing endpoints

Dashboard-level configuration — requires an authenticated session, never a secret or public key:

GET    /v1/environments/{env}/webhooks
POST   /v1/environments/{env}/webhooks
PATCH  /v1/webhooks/{id}
DELETE /v1/webhooks/{id}
POST   /v1/webhooks/{id}/rotate-secret
POST   /v1/webhooks/{id}/test
GET    /v1/webhooks/{id}/deliveries

The endpoint secret (whsec_...) is shown once, at creation or rotation time, and never returned by the API again. POST /v1/webhooks/{id}/test sends a real webhook.test delivery through the identical signing/delivery pipeline as any other event — it is never a bypass path.