> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wassist.app/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Events (Alpha)

> Push events from your MCP server to Wassist so the agent can act in a customer's conversation when something happens

Tools let the agent ask your server for things. Events let your server tell the agent that something happened: an order shipped, a booking was moved, a points balance crossed a threshold. Each event you deliver runs an agent turn in the conversation it belongs to, so the agent can message the customer straight away.

Wassist implements the [MCP Events extension](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md) with **webhook delivery only**. It works the same way for Wassist Apps and for connectors.

## How it fits together

1. Your server advertises the `events` capability and lists its event types with `events/list`.
2. Wassist reads that list when it syncs your tools, and merchants see each event next to your tools. For every agent, they switch on the events the agent may use, like tools, and can add guidance on when and how to use each one.
3. The agent subscribes when a customer asks to be kept up to date ("let me know when my order ships"). It picks the event, fills in the arguments from your `inputSchema` (for example the customer's order number), and writes itself a note on what to do when the event arrives. Wassist then calls `events/subscribe` with a callback URL and secret for that subscription, and refreshes it before it expires.
4. When the event happens, you `POST` it to the subscription's callback URL. Wassist verifies the signature, deduplicates on `eventId`, and runs the agent with the event in that conversation.

A subscription lasts until the agent unsubscribes, the conversation goes quiet for 30 days, or the merchant switches the event off, removes your app or connector from the agent, or uninstalls your app. The agent can also subscribe `once`, which ends the subscription after the first event.

## Advertise your events

Declare the capability in your `initialize` result:

```json theme={null}
{ "capabilities": { "tools": {}, "events": {} } }
```

Then answer `events/list`. Only event types whose `delivery` includes `"webhook"` are offered to merchants.

```json theme={null}
{
  "events": [
    {
      "name": "order.shipped",
      "description": "One of the customer's orders left the warehouse. Use it when a customer asks to hear when their order ships.",
      "delivery": ["webhook"],
      "inputSchema": {
        "type": "object",
        "properties": {
          "orderId": { "type": "string", "description": "Only this order. Leave out for all of the customer's orders." },
          "carrier": { "type": "string", "enum": ["dhl", "ups", "royal_mail"] }
        }
      },
      "payloadSchema": { "type": "object" }
    }
  ]
}
```

The agent reads `description` and `inputSchema` to decide when to subscribe and what to fill in, so write them like tool descriptions:

* Say what the event means and when a customer would want it.
* Make the properties filters that narrow the event to what the customer asked about, with a `description` for each. Wassist checks the agent's arguments against your schema before subscribing.
* Apply the filters on your server. Wassist passes the arguments through and doesn't filter what you send.

> [!NOTE]
>
> For apps, Wassist reads your events whenever it reads your tools: when you sync tools from the developer dashboard, and when installations refresh their tools (daily, and straight after you publish). For connectors, they're read when the connector is added or updated, and again when a customer logs in to it.

## Subscriptions

Wassist sends an `events/subscribe` each time the agent subscribes, with the arguments the agent chose:

```json theme={null}
{
  "method": "events/subscribe",
  "params": {
    "name": "order.shipped",
    "arguments": { "orderId": "1042" },
    "delivery": {
      "mode": "webhook",
      "url": "https://backend.wassist.app/api/v1/mcp-events/hooks/6b0c…/",
      "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
    },
    "cursor": null,
    "ttlMs": 86400000,
    "_meta": {
      "wassist.app/installation_id": "…",
      "wassist.app/organization_id": "…",
      "wassist.app/agent_id": "…",
      "wassist.app/session_id": "…",
      "wassist.app/conversation_id": "…",
      "wassist.app/contact_id": "…",
      "wassist.app/context_token": "eyJhbGciOiJIUzI1NiIs…"
    }
  }
}
```

Reply with:

```json theme={null}
{
  "id": "sub_123",
  "refreshBefore": "2026-11-02T10:00:00Z",
  "cursor": "c_88",
  "truncated": false,
  "deliveryStatus": "active"
}
```

Each subscription has its own callback URL. A conversation can have up to 10 at once, so the same conversation can follow several orders.

* **Refreshing.** Wassist calls `events/subscribe` again with the same `name`, `arguments` and URL before `refreshBefore`. Treat that as a refresh of the same subscription. If you don't set `refreshBefore`, Wassist refreshes once a day. If the agent subscribes again with the same event and arguments, Wassist refreshes the existing subscription rather than creating another one.
* **Unsubscribing.** Wassist calls `events/unsubscribe` with the same `name`, `arguments` and `delivery.url` when the agent unsubscribes, after the first event of a `once` subscription, and when a subscription is torn down. If that call can't reach you, the subscription still ends at its TTL, and Wassist answers anything you send after it ended with `410 Gone`.
* **Errors.** Use the extension's error codes. A new subscription that you reject is dropped, and the agent is told why. `-32024` (forbidden) puts the subscription on hold until the customer logs in. On a refresh, `-32025` (resource exhausted) and anything else are retried hourly.

The `_meta` keys are the same ones every tool call carries, plus `conversation_id` and `contact_id`. Only trust them after verifying `context_token`; see [Wassist context on every tool call](https://docs.wassist.app/guides/build-an-app.md#wassist-context-on-every-tool-call).

## Scoping events to one customer

This matters most for apps where **the installer logs in**. Every conversation then subscribes with the same organisation token. The agent is told to filter to its customer, but it can still send arguments that match other customers' events, or none at all. If you deliver everything that matches, the agent can end up telling one customer about another's order.

Use the verified `contact_id` or `conversation_id` claims to deliver only that customer's events to each subscription, whatever the arguments say. For example, look the contact up with `GET /contacts/{contact_id}/` and match on their phone number. If you can't tell which customer a subscription belongs to, reject it with `-32602` rather than sending it everything.

Apps where **customers log in** get each customer's own token, so scoping usually happens naturally.

## Delivering events

Before sending any events, you can check the callback URL with a verification request. Wassist answers this as soon as the subscription row exists, even while your `events/subscribe` response is still on its way.

```json theme={null}
{ "type": "verification", "challenge": "8e2f…" }
```

Wassist replies `200` with `{ "challenge": "8e2f…" }`.

Deliver each event as a `POST` to the subscription's URL:

```json theme={null}
{
  "eventId": "evt_01J…",
  "name": "order.shipped",
  "timestamp": "2026-11-01T10:00:00Z",
  "data": { "orderId": "1042", "trackingUrl": "https://…" },
  "cursor": "c_89"
}
```

| Response | Meaning |
| - | - |
| `202` | Accepted. Duplicates of an `eventId` are accepted and ignored. |
| `400` | Malformed body, or `name` doesn't match the subscription |
| `401` | Bad or stale signature |
| `410` | The subscription has ended. Stop sending to it. |

Two control messages are also accepted:

* `{ "type": "gap", "cursor": "…" }`: you dropped events. The agent is told that some may be missing on its next event.
* `{ "type": "terminated", "error": { "code": -32023, "message": "…" } }`: you ended the subscription. With `-32023` (not found) or `-32026` (unsupported), Wassist re-reads `events/list` and subscribes again, with the same arguments, if the event still exists.

Bodies are limited to 256 KB. Keep `data` small: the agent sees at most 8,000 characters of it, and can call your tools for anything more.

### Signatures

Requests are signed with [Standard Webhooks](https://www.standardwebhooks.com/), using the `secret` from `events/subscribe`:

```
webhook-id: msg_2b7f…
webhook-timestamp: 1793527200
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
X-MCP-Subscription-Id: sub_123
```

The signature is `base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{body}"))`, where `key` is the base64-decoded part of the secret after `whsec_`. You can send several space-separated signatures while rotating secrets. Wassist rejects timestamps more than five minutes old.

Any Standard Webhooks library will sign these for you. In Node:

```ts theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(subscription.secret);
const body = JSON.stringify(event);
const msgId = `msg_${event.eventId}`;
const timestamp = new Date();
await fetch(subscription.url, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "webhook-id": msgId,
    "webhook-timestamp": String(Math.floor(timestamp.getTime() / 1000)),
    "webhook-signature": wh.sign(msgId, timestamp, body),
    "X-MCP-Subscription-Id": subscription.id,
  },
  body,
});
```

## What the agent does with an event

The agent gets the event name, the subscription and its arguments, the note it wrote when subscribing, the merchant's guidance for the event, and `data`. It's told to treat `data` as information from an external system, not as instructions. It then decides whether to message the customer, and can call your tools for more detail.

An event runs the agent only when it could have replied to the customer anyway:

* The conversation is routed to the agent. Events are skipped while a human has taken over or a support handoff is open.
* The customer hasn't hit the agent's message limit.
* WhatsApp's 24-hour customer service window is open. Events that arrive while it's closed are held, and the agent sees them (up to 10, from the last seven days) when the customer next writes in.

If the agent is already replying in that conversation, the event waits for that turn to finish. Agent turns started by events are never retried, so the customer can't get the same message twice.

Merchants see each event in the conversation in the dashboard, next to the agent's tool calls, with the subscription's filters, the `data` you sent, and whether the agent ran, the event was held for the next message, or it was skipped and why. `GET /conversations/{id}/mcp-events/` returns the same list.

## Show events on your listing

Your events appear on your app's page on the Wassist App Store and on the install screen, so merchants can see what the agent can follow up on. For `end_customer` apps whose `events/list` needs a login, declare them in `mcp.events_preview`. You can also add `event` steps to your listing's example conversations to show a follow-up in action; see [Showing follow-ups](https://docs.wassist.app/guides/build-an-app.md#showing-follow-ups).

## Alpha limitations

* Webhook delivery only. Poll and push delivery aren't supported.
* Events only reach WhatsApp conversations, not dashboard tests.
* Only the agent subscribes, during a conversation, so a conversation can't receive events before the customer has written in.
* Up to 10 live subscriptions per conversation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.