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

# Webhooks and security

> How Reintersect signs and delivers requests to your app, the 5 second interactive deadline, deferred answers, idempotency and authorization.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

Reintersect talks to your app with signed HTTPS `POST` requests to one endpoint. A self-hosted app
receives them at the `endpoint` URL it declares; a hosted app receives them inside Reintersect's
runtime. The SDK verifies and routes every request for you. This page explains what it checks, so you
can reason about failures and implement the protocol yourself if you need to.

## Two lanes

| | Interactive | Lifecycle |
| - | - | - |
| Used for | Rendering blocks, running actions, inserting blocks, select search, resolving and searching references, slash commands, message actions | [Events](/events/overview) |
| Who waits | A member or an agent, right now | Nobody |
| Deadline | 5 seconds; 30 seconds for commands and message actions | Answer promptly with `2xx` |
| Retries | None | Up to 20 attempts over about three days |
| Header `reintersect-delivery` | `interactive` | `lifecycle` |

The full list of interactive delivery types and their payloads is in the
[delivery protocol](/reference/delivery-protocol).

## Signatures

Every request follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification and
carries these headers:

| Header | Value |
| - | - |
| `webhook-id` | A unique ID for the delivery. Retries of a lifecycle delivery reuse it. |
| `webhook-timestamp` | Unix time in seconds when Reintersect signed the request. |
| `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 of `webhook-id`, `webhook-timestamp` and the raw body joined with `.`, using your app's signing secret. During a secret rotation, several space-separated signatures can be present. |
| `reintersect-delivery` | `interactive` or `lifecycle`. |
| `reintersect-install` | The ID of the install the request belongs to. |

Your app's signing secret is in **Settings → Developer → Apps → your app**, under the app's
credentials. Rotating it starts a 24 hour grace period in which Reintersect signs with both the old
and the new secret, so you can deploy the new value without dropping requests. Hosted apps are
redeployed with the new secret automatically.

### Verification in the SDK

The SDK verifies every request before your code runs, with WebCrypto, on every runtime it supports.
It rejects a request when:

* the signature doesn't match any signature in `webhook-signature`, or
* `webhook-timestamp` is more than 5 minutes away from the current time.

Rejected requests get a `401` response and never reach your handlers. The SDK also remembers each
`webhook-id` for 10 minutes on the same instance. A repeated lifecycle delivery is answered with
`{ "duplicate": true }` without running your handler again, and a repeated interactive delivery,
such as a replayed button click, is refused with a `400` (`replayed`).

Where the secret comes from:

| Runtime | Secret |
| - | - |
| Self-hosted | Set `REINTERSECT_APP_SIGNING_SECRET` in your server's environment. The SDK reads it from the environment, or from the `env` bindings passed to `app.fetch(request, env)`. |
| Reintersect-hosted, production | Reintersect binds `REINTERSECT_APP_SIGNING_SECRET` to the Worker. |
| Reintersect-hosted, preview | Reintersect binds a separate secret derived from the app's (`whsec_` plus an HMAC of the app's key) and signs deliveries to the preview with it, so code under review never holds the secret your production deploy verifies with. |
| `reintersect dev` | The CLI fetches the development app's secret once and keeps it in `.reintersect/dev/session.json` (readable only by you). |

### Verifying without the SDK

Any Standard Webhooks library works. With the `standardwebhooks` package:

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

const webhook = new Webhook(process.env.REINTERSECT_APP_SIGNING_SECRET ?? "");

export async function POST(request: Request) {
  const body = await request.text();

  const delivery = webhook.verify(body, {
    "webhook-id": request.headers.get("webhook-id") ?? "",
    "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
    "webhook-signature": request.headers.get("webhook-signature") ?? "",
  });

  // `delivery` is the parsed, verified envelope: { id, type, timestamp, app, install, actor, data, … }
  return Response.json(await handle(delivery));
}
```

`verify` throws when the signature or timestamp is invalid. Verify the raw body before you parse it;
re-serialized JSON doesn't match the signature.

## Interactive deliveries

When a member clicks a button, Reintersect sends a `block.action` delivery and waits up to 5 seconds.
Your app answers in one of two ways.

### Answer within 5 seconds

Respond `200` with the result: the next state, a notice, field errors and so on. With the SDK, that is
whatever your action handler returns; the SDK merges the state, renders the block and sends both back
in the same response. The member sees the new block as soon as the response arrives.

If your app doesn't answer in time, answers with an error, or returns a result that fails
validation, Reintersect keeps the last good version of the block and shows the member an inline
error. Interactive deliveries are never retried, because the member may already have moved on or
clicked again.

### Defer and push later

When the work takes longer, such as a deployment, acknowledge first and push the result when it is
ready. With the SDK, return a quick result and continue in `waitUntil`:

```ts theme={null}
export const promote = defineAction(
  "promote",
  {
    args: z.object({ deploymentId: z.string() }),
    state: z.object({ status: z.enum(["waiting", "deploying", "shipped", "failed"]) }),
    destructive: true,
  },
  async ({ args, block, reintersect, waitUntil }) => {
    waitUntil(
      promoteDeployment(args.deploymentId).then(
        () => reintersect.blocks.update(LaunchBlock, block.id, { state: { status: "shipped" } }),
        () => reintersect.blocks.update(LaunchBlock, block.id, { state: { status: "failed" } }),
      ),
    );

    return { state: { status: "deploying" }, notice: "Promotion started" };
  },
);
```

Inside an action, `reintersect` is authorized by the delivery's interaction token, so the push
works without extra credentials.

Without the SDK, respond `202 Accepted` with an empty body, then push the result within 15 minutes.
The `block.action` payload includes an `interactionToken`. Send it as a bearer token to
[update](/api-reference/blocks/update) or [replace](/api-reference/blocks/replace) that block's
state:

```bash theme={null}
curl -X PATCH https://api.reintersect.com/v0/blocks/blk_44az \
  -H "authorization: Bearer $INTERACTION_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "state": { "status": "shipped" } }'
```

An interaction token is valid for 15 minutes and only for the block the action ran on. After it
expires, use the install's credential, which needs the `blocks:write` scope.

### Commands and message actions

[Slash commands](/commands) and [message actions](/message-actions) defer the same way, except the
`202` body says who sees your app thinking: `{ "visibility": "private" }` for only the member who ran
it, or `{ "visibility": "public" }` for everyone there. Then send the answer within 15 minutes to
[`POST /v0/invocations/respond`](/api-reference/invocations/respond) with the delivery's
`interactionToken` in the body and your install token as the credential. With the SDK, return
`defer(task, { visibility })` from the handler, or `defer()` and call
`reintersect.invocations.respond` when your work finishes. A run takes one answer; the token is refused after that or after 15 minutes.

## Idempotency

| Situation | What to use |
| - | - |
| A lifecycle event arrives twice | `webhook-id`, the same on every retry. The SDK skips a `webhook-id` it handled in the last 10 minutes on the same instance; across instances or restarts, use each object's `version` or your own records. |
| A member runs a command or message action twice | Each run is a separate invocation with its own `invocationId` and `interactionToken`. Make the work safe to repeat, or check your own state first. |
| A member clicks a button twice | Each click is a separate invocation with its own `invocationId`. Reintersect disables the button while one is pending, but a member can click again after an error. Make actions safe to repeat, or check your own state first. |
| Two pushes race for the same block | Pass `expectedVersion` with [`update`](/api-reference/blocks/update). If the block changed in between, the request fails with `ReintersectAppBlockVersionConflictError`; read the block and try again. |
| An agent runs an action | Mark actions that are safe to repeat with `idempotent: true`; the hint is shown to the agent with the tool. |

## Authorization

Before it sends an action, Reintersect checks that:

* the member can see and write in the circle;
* the block's [audience](/blocks/audience-and-lifecycle) includes them;
* the block isn't final;
* the action is rendered in the block's current revision;
* the install is active.

It then sends the member as `actor` in the signed body.

Your app still decides what that member may do in your own system. Use `actor.memberId` to look up
the matching account on your side before you deploy, delete or pay for anything. `audience` hides
components from people; it doesn't stop a crafted request, and your server is the last check.

## Network rules

* Self-hosted endpoints must use HTTPS on the default port. Reintersect doesn't deliver to private,
  loopback or link-local addresses.
* Reintersect doesn't follow redirects. An endpoint that answers `3xx` fails the delivery.
* Reintersect never sends your app its cookies or a member's session. The only credentials in a
  request are the signature headers, a short-lived install token in the body (`installToken`, valid
  for one hour) and, for actions, an interaction token for that block.
* Hosted apps can only call the hosts they list in `egress`. Reintersect's gateway sets the install
  token on their calls to the Reintersect API. See [Reintersect-hosted apps](/hosting/reintersect-hosted).

## Reintersect-signed tokens

Webhook deliveries are always signed with Standard Webhooks. Separately, MCP servers can opt in to
receive a short-lived JWT that Reintersect signs for each tool call, so they can identify the member
without running their own OAuth. See [MCP authentication](/mcp/authentication). Context that
Reintersect adds to MCP calls under `_meta` is a convenience hint and doesn't prove identity, so
authorize with the verified token and never with `_meta`.


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