Skip to main content
CanaryVaultsCanaryVaults home
ProductsPricingBlogDocs
Start Free

Reference

Alert channels & webhooks

Every fired canary sends an alert to whichever channels are actually configured. Email needs a working sender and Telegram needs a linked chat; an unconfigured built-in channel is skipped rather than queued, so if neither is set up the alert reaches nobody. On top of those you can fan the same alert out to Slack, Discord, or any HTTPS endpoint via an HMAC-signed webhook. Channels are configured per account under Settings → Alert channels, or through the /api/alert-channels endpoints.

Delivery semantics for all three kinds: 10 second timeout per attempt, up to 3 attempts with exponential backoff, and every delivery outcome is recorded in your account audit trail. Failures are surfaced per channel as lastError.

Slack setup

  1. In Slack, open your workspace's app directory and add (or open) the Incoming Webhooks app.
  2. Create a new webhook, pick the channel alerts should land in (for example #security-alerts), and copy the generated URL — it starts with https://hooks.slack.com/services/….
  3. In CanaryVaults, go to Settings → Alert channels → Add channel → Slack, paste the URL, and save.
  4. Press Test — a synthetic alert (clearly marked, no canary involved) should appear in the channel within seconds.

Alerts render as Slack blocks: a headline with the alert level and platform, followed by attacker IP, location, attacker type, and the risk score.

Discord setup

  1. In Discord, open the target channel's settings: Edit channel → Integrations → Webhooks → New webhook.
  2. Name it (for example CanaryVaults), then Copy webhook URL — it looks like https://discord.com/api/webhooks/<id>/<token>.
  3. In CanaryVaults, add a channel of kind Discord and paste the URL.
  4. Press Test to verify delivery. Alerts arrive as an embed, color-coded by alert level.

Generic webhook setup

  1. Stand up an HTTPS endpoint that accepts POST application/json. Private/loopback addresses are rejected — the endpoint must be publicly reachable.
  2. Generate a shared secret (at least 16 characters, e.g. openssl rand -hex 24) and store it in your receiver's secret manager.
  3. Add a channel of kind Webhook with the URL and the secret. CanaryVaults signs every delivery with it.
  4. Verify the signature on every request before trusting the payload (snippets below), then press Test.

Each delivery carries these headers:

Content-Type: application/json
User-Agent: CanaryVaults-Webhook/1.0
X-CanaryVaults-Event: canary.tripwire_triggered
X-CanaryVaults-Timestamp: 1784887923        # unix seconds at send time
X-CanaryVaults-Signature: sha256=8f2a...    # HMAC-SHA256(secret, "{timestamp}.{raw_body}")

Payload schema

The body is stable JSON (schema version 1). Test alerts use event: "canary.test_alert" and set data.test: true — never treat them as incidents.

{
  "version": "1",
  "event": "canary.tripwire_triggered",   // or "canary.test_alert"
  "sentAt": "2026-07-19T10:12:03Z",
  "data": {
    "event": "canary.tripwire_triggered",
    "alertLevel": "CRITICAL",             // CRITICAL | HIGH | MEDIUM
    "platform": "gmail",                  // where the canary credential was used
    "canaryId": "canary_abc123",
    "canaryAgeDays": 4,
    "attacker": {
      "ip": "203.0.113.10",
      "country": "NL",
      "city": "Amsterdam",
      "org": "Example Hosting BV",
      "type": "VPN_PROXY_USER",
      "abuseScore": 67                    // 0-100 (AbuseIPDB confidence)
    },
    "riskScore": 72,                      // 0-100 composite risk
    "riskLevel": "HIGH",
    "interpretation": "ACTIVE PIPELINE: Credential stolen within 1 week.",
    "threatEventId": "5f1c...",           // null for test alerts
    "triggeredAt": "2026-07-19T10:12:01Z",
    "test": true                          // only present on test alerts
  }
}

Verifying signatures

Recompute the HMAC over {timestamp}.{raw_body} using the exact bytes you received (do not re-serialize the JSON), compare with a constant-time comparison, and reject timestamps older than a few minutes to block replays.

import hashlib, hmac, time

def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    # A missing or non-numeric header is hostile input, not a bug: return
    # False rather than raising out of your handler.
    try:
        sent_at = int(timestamp)
    except (TypeError, ValueError):
        return False
    # Reject stale deliveries (replay protection)
    if abs(time.time() - sent_at) > 300:
        return False
    signed = f"{timestamp}.".encode() + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

# In your handler:
# verify(SECRET,
#        request.headers["X-CanaryVaults-Timestamp"],
#        request.raw_body,               # the exact bytes, NOT re-serialized JSON
#        request.headers["X-CanaryVaults-Signature"])
const crypto = require("crypto");

function verify(secret, timestamp, rawBody, signature) {
  // Number(undefined) is NaN and every NaN comparison is false, so a missing
  // header would have slipped past the replay window unchecked.
  const sentAt = Number(timestamp);
  if (!Number.isFinite(sentAt)) return false;
  if (Math.abs(Date.now() / 1000 - sentAt) > 300) return false;
  const signed = Buffer.concat([Buffer.from(timestamp + "."), rawBody]);
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  // timingSafeEqual throws RangeError unless both buffers are the same
  // length -- and a missing or truncated signature is exactly the hostile
  // case this function exists to reject. Compare lengths first.
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

Retries, failures, and testing

A delivery attempt counts as successful on a 2xx response and nothing else. Redirects are not followed, so a 3xx means the configured URL is wrong: it is reported as a failure and not retried. Network errors and 5xx are retried up to 3 total attempts with exponential backoff; 4xx responses other than 408/409/425/429 are not retried. After a failed delivery the channel shows lastError in Settings and the failure is written to your audit trail. Disabled channels are skipped entirely.

# fire a synthetic alert through a configured channel
curl -X POST https://api.canaryvaults.com/api/alert-channels/<channel-id>/test \
  -H "Authorization: Bearer $CV_TOKEN"
CanaryVaults

Deception-based AI security. Decoys, trap facts, honeypots, prompt defense, and tamper-evident audit trails — one workspace.

Plant your first canary

PRODUCT

ProductsCanaryAgentDashboardPricingReferralGet started

RESOURCES

DocumentationQuickstartShieldEvidence formatAPIBlog

COMPANY

AboutSecurityReport a vulnerabilityContact

TRUST

Trust centerVerify evidenceStatusChangelogIncidentsDPA

COMPARE

vs Thinkst Canaryvs CanaryTokensFor SaaS teams

LEGAL

TermsPrivacyCookiesSubprocessorsSupport
deception-based AI security© CanaryVaults · canaryvaults.comsha-256 sealed · tamper-evident

CANARYVAULTS