# Sublime Security Webhooks

## When to Use This Skill

* How do I receive Sublime Security webhooks?
* How do I verify the `X-Sublime-Signature` header (`t=...,v0=...`)?
* How do I handle `message.flagged`, `group`, or `manual` events?
* Why is my Sublime Security webhook signature verification failing?
* How do I send flagged email alerts from Sublime to my own service?

## Essential Code (USE THIS)

Sublime signs every webhook Action delivery with `X-Sublime-Signature`:

```
X-Sublime-Signature: t=1626139320,v0=3d35d777510a7ccf73ded723f141b0ada2986aebe63c4e45a9c7e4bb1abaca06

```

* `t` is the Unix timestamp (seconds). Each signature is prefixed by a scheme (`v` plus an integer); `v0` is the only scheme today. There is **no space** after the comma.
* Each `v0` value is the lowercase hex HMAC-SHA256 of `"<t>.<raw body>"`, keyed with the webhook Action's **signing secret** (Actions → select the webhook Action → click to reveal), used as UTF-8 bytes.
* The header can carry several `v0` values. Accept the request if **any** one matches.
* Check that each candidate is exactly 64 hex characters before decoding: `Buffer.from(x, 'hex')` silently truncates invalid hex. Hex is case-insensitive.
* Missing or invalid signature: respond `401`. Respond `2xx` within 10 seconds on success.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifySublime(rawBody, signatureHeader, secret) {
  if (!secret || !signatureHeader) return false;
  let timestamp;
  const signatures = [];
  for (const item of signatureHeader.split(',')) {
    const i = item.indexOf('=');
    if (i <= 0) continue;
    const key = item.slice(0, i).trim(), value = item.slice(i + 1).trim();
    if (key === 't') timestamp = value;
    else if (key === 'v0') signatures.push(value);
  }
  if (!/^\d+$/.test(timestamp ?? '') || Number(timestamp) <= 0 || signatures.length === 0) return false;
  // Optional replay window, off by default: Hookdeck retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((sig) =>
    /^[0-9a-fA-F]{64}$/.test(sig) && crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected));
}

```

### Verification (Python)

```python
import hashlib, hmac, re

def verify_sublime(raw_body: bytes, signature_header, secret: str) -> bool:
    if not secret or not signature_header:
        return False
    timestamp, signatures = None, []
    for item in signature_header.split(","):
        key, sep, value = item.partition("=")
        key, value = key.strip(), value.strip()
        if not sep or not key:
            continue
        if key == "t":
            timestamp = value
        elif key == "v0":
            signatures.append(value)
    if not timestamp or not re.fullmatch(r"[0-9]+", timestamp) or int(timestamp) <= 0 or not signatures:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(re.fullmatch(r"[0-9a-fA-F]{64}", s) and hmac.compare_digest(s.lower().encode(), expected.encode())
               for s in signatures)

```

### Replay window (opt-in)

The examples do **not** enforce a timestamp window by default. Sublime's own retries arrive with a new timestamp and signature, but Hookdeck retries and replays resend the original request with the original `t`, so a window would reject late Hookdeck redeliveries. To opt in, set `SUBLIME_SECURITY_TIMESTAMP_TOLERANCE_SECONDS` to a positive whole number; any other value leaves it off, with a warning at startup. Use the body's `id` for idempotency instead.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sublime-security-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sublime-security-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sublime-security-webhooks/examples/fastapi/).

## Common Event Types

The body is JSON: `{ id, api_version, created_at, type, data }`. Dispatch on `type`, which says why the delivery was sent. `data` holds `message`, `flagged_rules`, and `triggered_actions`.

| `type` | Description |
| --- | --- |
| `message.flagged` | A message matched a detection rule and the webhook Action fired |
| `group` | The Action fired for a message group |
| `manual` | A user sent the message to the webhook Action by hand |

`type_extended` may list several reasons at once and adds `group_late_arriving`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sublime-security-webhooks/references/overview.md).

> **For the full payload reference**, see [Sublime's webhook documentation](https://docs.sublime.security/docs/webhooks).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Sublime-Signature` | `t=<unix seconds>,v0=<hex>[,v0=<hex>...]` |

## Environment Variables

```bash
SUBLIME_SECURITY_WEBHOOK_SECRET=your_webhook_action_signing_secret   # Actions → webhook Action → click to reveal
SUBLIME_SECURITY_TIMESTAMP_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 sublime-security --path /webhooks/sublime-security

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sublime-security-webhooks/references/overview.md) - Sublime webhook Actions, event types, payload structure
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sublime-security-webhooks/references/setup.md) - Creating a webhook Action and finding the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sublime-security-webhooks/references/verification.md) - Signature verification details and gotchas