# Missive Webhooks

Missive is a shared team inbox. A **Webhook** action on a Missive rule POSTs a
JSON payload (the rule, the conversation and the latest message or comment) to
your endpoint whenever the rule triggers, for example on incoming email or a
label change.

## When to Use This Skill

* How do I receive Missive webhooks from a rule?
* How do I verify the Missive `X-Hook-Signature` header?
* How do I handle a Missive `label_change` rule webhook?
* Why is my Missive webhook signature verification failing?
* How do I validate the request Missive sends when I save a rule?

## Verification (core)

If you set a **validation secret** on the rule's webhook action, Missive sends
`X-Hook-Signature: sha256=<hex>`: HMAC-SHA256 of the **raw request body**,
keyed with that secret, hex encoded. Require and strip the `sha256=` prefix,
check the rest is exactly 64 hex characters *before* decoding
(`Buffer.from(x, 'hex')` silently truncates invalid hex), then compare in
constant time:

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

function verifyMissiveSignature(rawBody, header, secret) {
  if (typeof header !== 'string' || !header.startsWith('sha256=')) return false;
  const sig = header.slice('sha256='.length);
  if (!/^[0-9a-f]{64}$/i.test(sig)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(sig, 'hex'));
}

```

Python (`hmac.compare_digest` on bytes, so a non-ASCII header gives 401, not 500):

```python
import hashlib, hmac, re

def verify_missive_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header or not header.startswith("sha256="):
        return False
    sig = header[len("sha256="):]
    if not re.fullmatch(r"[0-9a-fA-F]{64}", sig):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest().encode()
    return hmac.compare_digest(expected, sig.lower().encode())

```

Read the **raw body** (`express.raw({ type: '*/*' })` so any content type
verifies, `request.text()`, `await request.body()`), not re-serialized JSON.
Respond `401` on a bad signature.

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

## Setup (including Hookdeck)

1. In Missive go to **Settings > Rules**, create a rule and choose **Webhook** as the action (creating rules needs an admin or owner of an organization on the Productive or Business plan).
2. Enter your endpoint URL and **set a validation secret** on the webhook action. Without one, deliveries are unsigned and fail verification, here and in a Hookdeck source.
3. Save the rule. Missive sends a **validation POST** to make sure your endpoint works, so the endpoint must be reachable (and accept POST) before you save.

Using Hookdeck: create the source with the same validation secret as the rule's
webhook action; Hookdeck verifies `X-Hook-Signature` before forwarding. Details
in [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/missive-webhooks/references/setup.md).

## Event Types (rule types)

Missive has no event header. Each payload has a `rule` object
(`id`, `description`, `type`) plus `conversation` and `latest_message` (or a
comment object for comment rules). **Dispatch on `rule.type`.**

| `rule.type` | Description |
| --- | --- |
| `label_change` | A shared label was added or removed (the example in Missive's docs) |

Missive's docs show only `label_change`; other rule triggers (incoming email,
outgoing email, new comment and so on) are chosen in the rule UI, so route
unknown types to a default branch and log them.

## Important Headers

| Header | Description |
| --- | --- |
| `X-Hook-Signature` | `sha256=` + HMAC-SHA256 hex of the raw body (only sent when a validation secret is set) |

## Delivery behaviour

* Endpoint must accept `POST` and respond within **15 seconds**.
* Errors or timeouts are retried up to **5 times over 8 minutes**.
* A rule that fails more than **50 times in a row** is disabled automatically; re-enable it in the Rules settings.

## Environment Variables

```bash
MISSIVE_WEBHOOK_SECRET=your_validation_secret   # The validation secret set on the rule's webhook action

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/missive-webhooks/references/overview.md) - What Missive webhooks are, rule types and payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/missive-webhooks/references/setup.md) - Create the rule webhook action, validation secret, Hookdeck
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/missive-webhooks/references/verification.md) - Signature verification details and gotchas