> ## 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.

# Delivery protocol

> The HTTP requests Reintersect sends to your app and the responses it expects, for implementers who don't use the SDK.

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

The SDK implements this protocol for you. Read this page if you build an app in another language, or
if you need the exact requests and responses to debug raw deliveries.

## One endpoint

Reintersect sends every delivery as an HTTPS `POST` to one URL: your app's `endpoint` for
self-hosted apps, or the path `/reintersect` of your Worker for hosted apps. The SDK treats any
request with `reintersect-delivery` and `webhook-signature` headers as a delivery. Each request
carries [Standard Webhooks](https://www.standardwebhooks.com) headers plus two of its own:

| Header | Value |
| - | - |
| `webhook-id` | The delivery id. Stable across retries of the same lifecycle delivery. |
| `webhook-timestamp` | Unix seconds when the request was signed. |
| `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 of `id.timestamp.body`, keyed with your app's signing secret. |
| `reintersect-delivery` | `interactive` or `lifecycle`. |
| `reintersect-install` | The install id. |
| `content-type` | `application/json` |

Verify the signature and reject timestamps more than 5 minutes from your clock before you parse the
body. See [Webhooks and security](/webhooks-and-security).

## Envelope

Every body has the same envelope. `type` says which delivery it is, and `data` holds its payload:

```ts theme={null}
interface Delivery<T extends string, D> {
  id: string;
  type: T;
  timestamp: string;
  app: { id: string; slug: string; version: number };
  install: { id: string; workspaceId: string; kind: "workspace" | "dev"; config?: Record<string, Json> };
  installToken: { token: string; expiresAt: string };
  actor?: { memberId: string; name: string };
  locale: string;
  timeZone: string;
  data: D;
}
```

| Field | Description |
| - | - |
| `id` | Same value as the `webhook-id` header. |
| `type` | One of the interactive or lifecycle types below. |
| `timestamp` | ISO 8601 time the delivery was created. |
| `app.version` | The version number the install is on. |
| `install.kind` | `dev` for a development install. Deliveries to development installs of self-hosted apps go through the `reintersect dev` relay. |
| `install.config` | The install config the admin set, when the app declares one. Secret fields are decrypted for you. |
| `installToken` | A one-hour install token (`reiapp_…`) for calls to the API while handling this delivery. |
| `actor` | The member who caused the delivery, when there's one, such as the member who clicked a button. |
| `locale`, `timeZone` | Currently always `en` and `UTC`. `Time` and `Duration` components format in each viewer's own settings, so prefer them to formatting dates yourself. |

## Interactive deliveries

Interactive deliveries happen while someone waits, for example when a member clicks a button, pastes
a link, opens a search tab, runs a slash command or picks a message action. You have 5 seconds to
answer, or 30 seconds for `command.run` and `messageAction.run`. Reintersect never retries an
interactive delivery. If your app fails or times out, the block keeps its last good tree and shows an
inline error, and a command or message action shows the member a failure card.

Answer `200` with the result in the table below, or `202` to finish later (see
[Deferred results](#deferred-results) and [Commands and message actions](#commands-and-message-actions)).

| `type` | `data` | `200` result |
| - | - | - |
| `block.render` | `{ block: { id, type, props, state, revision }, mode: "interactive" }` | `{ tree, actions }` |
| `block.action` | `{ block, action: { name, args }, input?, tallies, invocationId, interactionToken }` | See [Action result](#action-result) |
| `block.insert` | `{ type, props, target: { conversationId?, circleId? } }` | `{ state, tree, actions }` |
| `select.search` | `{ block, action: { name, args }, query }` | `{ options: Array<{ value, label, description? }> }`, at most 50 |
| `reference.resolve` | `{ items: Array<{ kind, patternId, params, url }> }`, at most 20 | `{ items: Array<ReferenceResolution> }`, in the same order |
| `reference.search` | `{ kind, query, limit }` | `{ items: Array<{ url, title, subtitle?, status? }> }` |
| `command.run` | `{ command, options, surface, invocationId, interactionToken }` | `InvocationAnswer`, see [Commands and message actions](#commands-and-message-actions) |
| `messageAction.run` | `{ id, message: { id, conversationId, text, author }, context, surface, invocationId, interactionToken }` | `InvocationAnswer` |

`tallies` on `block.action` maps each viewer input to its counts, such as
`{ verdict: { go: 4, nogo: 1 } }`. `invocationId` is unique per click. `input` holds form values, or
the picks of a choice group's `onChange`. A response that doesn't match the result schema fails the
call with `ReintersectAppResponseInvalidError` on Reintersect's side, and Reintersect refuses
responses over 1 MB.

In `block.insert`, `target.circleId` is set when the block goes into a circle, and
`target.conversationId` when its message is filed in a conversation. Neither is set for a block
outside a circle, such as a playground preview. The delivery never carries `replyTo`: an API insert
with `replyTo` arrives with that reply's circle, plus its conversation when it has one.

### Action result

Every field is optional:

```ts theme={null}
interface ActionResult {
  state?: Record<string, unknown>;
  tree?: BlockUiDocument;
  actions?: Record<string, { name: string; args?: unknown }>;
  notice?: { tone: "neutral" | "info" | "success" | "warning" | "danger"; text: string };
  fieldErrors?: Record<string, string>;
  openUrl?: string;
  final?: { summary?: string };
  notify?: Array<{ memberId: string; text: string }>;
  revalidate?: boolean;
}
```

| Field | Effect |
| - | - |
| `state` | Replaces the block's state. Reintersect validates it against the block's state schema. |
| `tree`, `actions` | The block's new tree and its action table. Send both together. |
| `notice` | A short message, at most 140 characters, shown to the member who acted. |
| `fieldErrors` | Errors keyed by form field name, at most 200 characters each, shown beside each field. |
| `openUrl` | An `https://` URL to open for the member who clicked, once and best effort; the block also shows an **Open \<host>** link. |
| `final` | Locks the block, with an optional summary of at most 280 characters. Reintersect stops calling your app for it and keeps the last tree. |
| `notify` | Up to 20 notifications of at most 280 characters, linking to the block. Needs `notifications:write`. |
| `revalidate` | Asks Reintersect to send `block.render` right after this response. |

For an action whose manifest hint is `readOnly`, Reintersect ignores `state`, `tree`, `actions`,
`final` and `notify`.

The SDK merges partial state from your handler before it answers, so the wire `state` is always
the full state.

### Reference resolution

```ts theme={null}
type ReferenceResolution =
  | GrantedResolution
  | { url: string; access: "unauthorized" | "forbidden" | "notFound" }
  | { url: string; access: "unavailable" };

interface GrantedResolution {
  url: string;
  access: "granted";
  visibility: "public" | "restricted";
  externalId: string;
  title: string;
  subtitle?: string;
  icon?: string;
  status?: { state: StatusState; label: string };
  updatedAt?: string;
  summary: string;
  block?: { tree: BlockUiDocument; actions: Record<string, { name: string; args?: unknown }> };
}
```

`unavailable` means the reference couldn't be resolved right now. Reintersect doesn't cache it and
resolves again later. The SDK sends it when a `resolve` handler throws. `externalId` identifies the
object: every occurrence of the same `kind` and `externalId` in a workspace shares one record, so one
update reaches all of them. See
[Resolving references](/references/resolving).

### Deferred results

If an action needs more than a few seconds, answer `202 Accepted` with an empty body, keep working,
and push the result with the `interactionToken` from the delivery:

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

The interaction token is valid for 15 minutes and only for the block the delivery is about. While
the block waits, the member sees the button's pending state. See
[Update a block](/api-reference/blocks/update).

### Commands and message actions

`command.run` and `messageAction.run` carry the run's identity and where it happened:

```ts theme={null}
type Surface =
  | { kind: "circle"; circleId: string; conversationId: string | null }
  | { kind: "dm"; partnerMemberId: string };

interface CommandRunData {
  command: string;
  options: Record<string, OptionValue>;
  surface: Surface;
  invocationId: string;
  interactionToken: string;
}

type OptionValue =
  | string
  | number
  | boolean
  | { kind: "member"; memberId: string; name: string }
  | { kind: "circle"; circleId: string; name: string }
  | { kind: "appObject"; id: string; title: string };

interface MessageActionRunData {
  id: string;
  message: { id: string; conversationId: string | null; text: string; author: EventAuthor };
  context: {
    level: "message" | "thread" | "conversation";
    message: ContextMessage;
    thread?: ContextMessage[];
    conversation?: {
      id: string;
      title: string;
      status: string | null;
      agreed: string | null;
      participants: EventAuthor[];
      messages: ContextMessage[];
    };
    surrounding?: ContextMessage[];
  };
  surface: Surface;
  invocationId: string;
  interactionToken: string;
}

interface ContextMessage {
  id: string;
  text: string;
  author: EventAuthor;
  createdAt: string;
}
```

`options` holds only the options the member filled in, already checked against your manifest.
`message.conversationId` is `null` for a chat message not filed in a conversation and for a direct
message. `context.level` is the level the install's version declares; see
[Message actions](/message-actions#context) for what each level sends. `EventAuthor` is described in
the [event catalog](/events/catalog#shared-shapes).

Both answer `200` with an `InvocationAnswer`:

```ts theme={null}
interface InvocationAnswer {
  visibility: "public" | "private";
  content?: Document;
  block?: { type: string; props: Record<string, Json> };
  notice?: { tone: "neutral" | "info" | "success" | "warning" | "danger"; text: string };
}
```

A `danger` or `warning` notice with no content or block reports the run as failed. Or answer `202`
with an `InvocationDeferral`, and send the answer within 15 minutes to
[`POST /v0/invocations/respond`](/api-reference/invocations/respond) with the delivery's
`interactionToken` and your install token. See [Slash commands](/commands#answers) for how
Reintersect places each answer.

```ts theme={null}
interface InvocationDeferral {
  visibility: "public" | "private";
}
```

`visibility` decides who sees your app thinking while it works: everyone on the surface, or only the
member who ran it. Any other body fails the run. The answer you send later has its own `visibility`.

## Block trees

`tree` is a block-UI document: a JSON tree of catalog nodes, shaped like the documents Reintersect's
editor stores.

```json theme={null}
{
  "type": "uiBlock",
  "attrs": { "catalog": 1, "summary": "Launch v2: 4 go, 1 no go" },
  "content": [
    {
      "type": "uiHeader",
      "content": [{ "type": "uiHeaderTitle", "content": [{ "type": "text", "text": "Launch v2" }] }]
    },
    {
      "type": "uiActions",
      "content": [
        {
          "type": "uiButton",
          "attrs": { "buttonType": "action", "label": "Ship", "action": { "ref": "a0" } }
        }
      ]
    }
  ]
}
```

* `attrs.catalog` is the catalog version. It's `1` today.
* `attrs.summary` is required plain text, at most 280 characters.
* Node names start with `ui`. Rich text inside `uiProse` reuses the editor's `paragraph`, `heading`,
  list, `blockquote` and `codeBlock` nodes.
* The root's children go `uiHeader`, the body, one `uiActions`, then `uiFooter`. A button bound to a
  `destructive` action carries `"intent": "destructive"` and a `confirm` of `{ title, body? }`.
* A metric's `value` and its delta's `value` are finite numbers, and the metric's `format` says how
  to read them. Percent values are fractions: `0.08` is 8%.
* Reintersect validates every tree against the catalog and the [limits](/blocks/limits) and
  rejects a tree that doesn't pass.

Your output may use `src` image URLs and `avatarSrc`; Reintersect copies the images and stores asset
ids instead. To check a tree against the same validator Reintersect runs, send it to
[`POST /v0/apps/check/trees`](/api-reference/tooling/check-trees).

### Action references

Buttons, menu items, forms and choice groups refer to actions by short references such as `a0` and
`a1`. The `actions` table you return with each tree maps every reference to an action name and its
arguments:

```json theme={null}
{ "a0": { "name": "ship", "args": { "deploymentId": "dpl_123" } } }
```

Reintersect stores this table on its servers. Browsers only ever see the references, so a member
can't change the arguments by editing the request. When the member clicks, Reintersect looks up
the reference for the tree's revision and sends `block.action` with the stored name and arguments.
References are unique within one tree revision and mean nothing outside it.

## Lifecycle deliveries

Lifecycle deliveries report things that already happened, such as an install, a published decision
or a mention of your app. Reintersect stores them in an outbox and retries them, so your handler can
be slow or briefly unavailable.

* Answer any `2xx` within 15 seconds to acknowledge. The body is ignored.
* Reintersect retries other responses and timeouts with jittered exponential backoff from one minute
  up to six hours, at most 20 attempts.
* `410 Gone` pauses deliveries to that install.
* After three days of continuous failures, Reintersect pauses deliveries to the install and emails
  the app's owner. The developer resumes them from the app's Deliveries page.
* Order isn't guaranteed. Every object in a payload carries a `version`; ignore a delivery whose
  version is older than one you've already processed.
* A delivery can arrive more than once. Use `webhook-id` to skip duplicates.

The `type` is one of the events in the [event catalog](/events/catalog).


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