# Guide to Missive Webhooks: Features and Best Practices

Missive webhooks send conversation data to your systems when a Missive rule fires. Missive is a shared inbox for email, SMS, WhatsApp and chat, and webhooks are how an incoming message, a label change or a reassigned conversation can create a CRM record, open a ticket or start a workflow in another tool.

This guide covers how Missive webhooks are set up as rule actions, what the payload contains, how the optional signature works, and how retries and auto-disabling affect a production receiver.

## What are Missive webhooks?

In Missive, a webhook is an action on a rule. You create a rule, give it a type and conditions, and choose **Webhook** as the action. Whenever the rule matches, Missive sends an HTTP `POST` with a JSON payload to the webhook URL. So the "events" are whatever your rules match: incoming messages from a certain sender, outgoing replies, a label being applied, and so on.

The payload describes the rule that fired, the conversation, and its latest message, so you can usually act on it without calling the Missive API.

Signing is optional. If you set a validation secret on the rule's webhook action, every request carries an `X-Hook-Signature` header with `sha256=` and the hex HMAC-SHA256 of the request body. Without a secret, requests are unsigned.

## Missive webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Settings > Rules**, with **Webhook** as the rule's action |
| Who can create | Admins or owners of an organization on the Productive or Business plan |
| Triggers | The rule's type and conditions |
| Validation request | Sent when the rule is saved, to check the endpoint works |
| Signature header | `X-Hook-Signature`, only when a validation secret is set |
| Scheme | HMAC-SHA256, hex, prefixed with `sha256=` |
| Signed content | The full request body |
| Secret | The validation secret ("Signature secret") you set on the rule action |
| Response window | 15 seconds |
| Retries | Errors and timeouts, up to 5 times over 8 minutes |
| Auto-disable | After more than 50 failures in a row, until re-enabled in the Rules settings |
| Debugging | The **Show rule webhook events** preference shows each fired webhook in the conversation timeline |

Missive publishes no delivery ID and no source IP list.

## Rule types and the payload

Missive rules come in a few types: incoming messages, outgoing messages, and user actions such as a label change, a new comment, a conversation being closed or reopened, or a change of assignee or team. The payload's `rule.type` carries the type of the rule that fired. Missive's docs show two values, `label_change` and `incoming_email`, but don't list the rest, so log any type you don't recognise.

Here is Missive's example payload, trimmed:

```json
{
  "rule": {
    "id": "45408b30-aa3a-45n1-bh67-0a0cb8da9080",
    "description": "Notify elfs",
    "type": "label_change"
  },
  "conversation": {
    "id": "47a57b76-df42-4d8k-927x-80dbe5d87191",
    "subject": "Mordor GPS coordinates",
    "team": { "id": "2f618f9e-d3d4-4a01-b7d5-57124ab366b8", "name": "Hobbits" },
    "assignee_names": "Frodo Baggins",
    "shared_label_names": "Elfs",
    "messages_count": 1,
    "web_url": "https://mail.missiveapp.com/#inbox/conversations/47a57b76-df42-4d8k-927x-80dbe5d87191"
  },
  "latest_message": {
    "id": "86ef8bb8-269c-4959-a4f0-213db4e67844",
    "subject": "Fwd: Mordor GPS coordinates",
    "preview": "Hi Mr. Gamgee, I discovered something really disturbing about the Mordor coordinates we had.",
    "type": "email",
    "delivered_at": 1548415828,
    "from_field": { "name": "Samwise Gamgee", "address": "sam@fellowship.org" }
  }
}

```

* `rule.id` identifies the exact rule, which is often a better routing key than `rule.type` when several rules point at the same URL.
* `conversation` carries the team, assignees, shared labels, authors, counts, and links to open it in the app or on the web.
* `latest_message.type` is the channel, such as `email`. It is not the event type.
* When the webhook runs as a rule step, `rule` also has `parent_id`, `delay_seconds` and `delay_business_hours`.

For WhatsApp, the `id` values in `from_field` and `to_fields` are Business-Scoped User IDs (such as `whatsapp:US.13491208655302741918`), not phone numbers. Missive flags this as a breaking change for integrations that relied on phone numbers.

## Setting up Missive webhooks

1. Generate a long random validation secret, for example with `openssl rand -hex 32`, and store it server-side as `MISSIVE_WEBHOOK_SECRET`.
2. Make sure your endpoint is live and accepts `POST` requests. Missive sends a validation request when you save the rule.
3. In Missive, go to **Settings > Rules**, create a rule, and set its type and conditions.
4. Choose **Webhook** as the action, enter your URL, and set the validation secret.
5. Save the rule.

If you use Hookdeck, create a source with the Missive source type, paste the same validation secret into it, and use the Hookdeck source URL as the webhook URL. Set a validation secret on the rule's webhook action. Without one, deliveries are unsigned and fail verification in Hookdeck. Hookdeck accepts Missive's validation request when you save the rule, so the save succeeds. Missive doesn't document whether that request is signed, so it may appear in Hookdeck as a failed verification.

## Securing Missive webhooks

Read `X-Hook-Signature`, check that it starts with `sha256=`, and take the rest. Compute the hex HMAC-SHA256 of the raw request body with the validation secret as the key, and compare in constant time, as Missive asks.

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

function verifyMissiveSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret || !signatureHeader.startsWith('sha256=')) return false;
  const received = Buffer.from(signatureHeader.slice('sha256='.length).toLowerCase(), 'utf8');
  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
    'utf8'
  );
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/missive', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyMissiveSignature(req.body, req.get('x-hook-signature'), process.env.MISSIVE_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString('utf8'));
  // Route on payload.rule.id or payload.rule.type, then queue the work.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_missive_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret or not signature_header.startswith("sha256="):
        return False
    received = signature_header[len("sha256="):].lower()
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_missive_signature(raw_body, request.headers.get("x-hook-signature"),
#                          os.environ["MISSIVE_WEBHOOK_SECRET"])

```

Missive signs only the body and sends no timestamp, so there is no replay window to check.

> **Make Missive webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Hook-Signature` header on every delivery, answers well inside the 15-second window, and holds each conversation event until your service has processed it.

## Missive webhook limitations and pain points

### Retries are over in eight minutes

**The Problem:** A short outage on your side loses events for good.

**Why It Happens:** Missive retries an error or a timeout up to 5 times over 8 minutes. A deploy, a database failover or a slow dependency can easily last longer.

**Workarounds:**

* Keep the webhook route simple and return a `2xx` as soon as the request is verified and stored.
* After an outage, use the API or the conversations themselves to catch up on what you missed.

**How Hookdeck Can Help:** Hookdeck accepts Missive's deliveries as soon as they arrive, so the eight minutes aren't spent on your downtime. Events wait in Hookdeck and are delivered with [retries](/docs/retries) you configure.

### 50 failures in a row disable the rule

**The Problem:** Your integration goes quiet, and the rule shows as disabled.

**Why It Happens:** If a webhook rule fails more than 50 times in a row, Missive disables it. A busy inbox can produce 50 failures quickly.

**Workarounds:**

* Check the Rules settings after any incident and re-enable the rule.
* Turn on **Show rule webhook events** to confirm whether Missive fired the webhook for a conversation.

**How Hookdeck Can Help:** Because Hookdeck answers Missive straight away, failures in your service don't count against the rule, and Hookdeck's [issues](/docs/issues) alert you to the failures instead.

### Unsigned unless you set a secret

**The Problem:** Anyone who learns the URL can post fake conversation data.

**Why It Happens:** The validation secret is optional, and without it Missive sends no signature.

**Workarounds:**

* Set a validation secret on every webhook action.
* Reject requests that lack `X-Hook-Signature` when you expect one.

**How Hookdeck Can Help:** Hookdeck's Missive source verifies every request and marks unsigned ones as failed verification, which shows you any rule set up without a secret.

### No event catalogue or delivery ID

**The Problem:** You can't list every `rule.type` value in advance, and you can't tell a retry from a new event.

**Why It Happens:** Missive documents rule types by their labels in the app, with only two wire values in its examples, and the payload has no delivery ID.

**Workarounds:**

* Route on `rule.id`, which you control, and log unknown `rule.type` values.
* Build an idempotency key from `rule.id` and `latest_message.id`.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeats based on the body fields you choose, such as `rule.id` and `latest_message.id`.

## Best practices

### Verify against the raw body

Missive signs the full request body. Use `express.raw({ type: '*/*' })` in Express, `await request.text()` in Next.js and `await request.body()` in FastAPI.

### Have the endpoint ready before saving the rule

Missive sends a validation request on save. Deploy the receiver first, or point the rule at Hookdeck, so the save succeeds.

### Use one rule per job

Give each integration its own rule and route on `rule.id`. It keeps conditions simple and lets you disable one integration without affecting the others.

### Acknowledge within 15 seconds

Verify, store the payload, return `200`, and run the CRM or ticketing work in a background job, as Missive recommends. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Missive webhooks are rule actions that post the rule, the conversation and its latest message to your URL. Set a validation secret so each request carries `X-Hook-Signature`, verify the `sha256=` hex HMAC against the raw body, route on `rule.id`, and answer within 15 seconds, because retries end after eight minutes and 50 failures in a row disable the rule.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Missive signature and keeps each event until your service has handled it, so a deploy or an outage doesn't cost you conversations.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Missive webhooks reliably in minutes.