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

# Webhooks overview

> Get notified when something changes in your team

A webhook is an HTTP `POST` Inboxapp sends to your server when something changes: a message arrives, a thread is archived, a contact's status moves.

Create webhooks in **Settings → Webhooks** and choose the event types each one receives.

## The event

Every event has the same envelope:

```json theme={null}
{
  "id": "ck9v2m5nj0xp4wq7ybftrae8",
  "seq": 48213,
  "type": "message.received",
  "apiVersion": "2",
  "createdAt": "2026-09-14T10:32:00.000Z",
  "object": {
    "type": "message",
    "id": "p8rvk2m5j0xn4wq7ybftcael",
    "url": "/api/v2/messages/p8rvk2m5j0xn4wq7ybftcael"
  },
  "data": { "threadId": "l44e15irdq4db30i77cgphhx" }
}
```

| Field | Notes |
| - | - |
| `id` | Unique per event. Deduplicate on it |
| `seq` | Increases per team. Resume replay from it |
| `type` | `resource.action` |
| `object` | What changed, with the path to read it |
| `data` | Only the values of the change |

### Events are thin

Webhooks follow the thin-event pattern: an event names the object that changed and carries only the values of the change, never the object itself.

* **Small payloads.** An event is a few hundred bytes, whatever the size of the conversation behind it.
* **Out-of-order events are harmless.** You read the current state from `object.url`, so a late event can't overwrite newer data with old.
* **Idempotent by design.** Handling the same event twice reads the same state twice.

```typescript handler.ts theme={null}
import express from "express";

const app = express();

app.post("/webhooks/inbox", express.json(), async (request, response) => {
  const event = request.body;
  response.sendStatus(200);

  if (event.type !== "message.received") return;

  const result = await fetch(`https://inboxapp.com${event.object.url}`, {
    headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}` },
  });

  if (result.status === 404) return; // deleted since the event
  if (!result.ok) throw new Error(`Failed to read ${event.object.url}`);

  const message = await result.json();
  console.log("New message:", message.content);
});
```

`object.url` answers `404` once the object is deleted.

<Note>
  Verify each request before trusting it. See [Verifying webhook
  signatures](/v2/webhooks/verifying-signatures).
</Note>

## Event types

| Resource | Events | `data` |
| - | - | - |
| Thread | `thread.created`, `thread.archived`, `thread.unarchived`, `thread.typing` | `{}` |
| | `thread.deleted` | `{ accountLinkId, platform }` |
| | `thread.assigned`, `thread.unassigned` | `{ assigneeId }` |
| Message | `message.sent`, `message.received`, `message.edited` | `{ threadId }` |
| | `message.deleted` | `{ threadId, scope }` |
| | `message.reactionAdded`, `message.reactionRemoved` | `{ threadId, emoji, authorPlatformId }` |
| Contact | `contact.statusChanged` | `{ statusId }` |
| | `contact.tagsChanged` | `{ tagIds }` |
| | `contact.notesChanged` | `{ notes }` |
| | `contact.valuationChanged` | `{ valuation }` |
| | `contact.assigneeChanged` | `{ assigneeId }` |
| Tag | `tag.created`, `tag.updated`, `tag.deleted` | `{}` |
| Status | `status.created`, `status.updated`, `status.deleted` | `{}` |
| Campaign | `campaign.created`, `campaign.started`, `campaign.paused`, `campaign.resumed`, `campaign.completed` | `{}` |
| Target | `target.contacted`, `target.followUpSent`, `target.replied` | `{ threadId, messageId, contactId, stepNumber }` |

Each event type has its own page under **Event types** with its full schema.

* `thread.assigned` reports the thread's own assignee. `contact.assigneeChanged` reports the contact's default.
* Values in `data` are the ones that change wrote. They can be out of date by the time you read them.
* New event types and `data` fields can be added within v2. Ignore what you don't know.

## Delivery

Each event is delivered **once**, with no retries, and your response isn't checked. Events for every platform your team has linked arrive on the same webhook.

Your endpoint should:

1. Answer with a `2xx` quickly, and do the work afterwards
2. Deduplicate on `id`
3. Store the highest `seq` it has processed

## Replay missed events

Events are kept for 7 days, up to 25,000 per team. After downtime, read what you missed from the `seq` of the last event you processed:

```typescript replay.ts theme={null}
async function replay(
  afterSeq: number,
  handle: (event: unknown) => Promise<void>,
) {
  let cursor = afterSeq;

  while (true) {
    const response = await fetch(
      `https://inboxapp.com/api/v2/events?afterSeq=${cursor}&limit=1000`,
      { headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}` } },
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`${error.code}: ${error.message} (${error.requestId})`);
    }

    const page = await response.json();

    for (const event of page.data) await handle(event);

    if (page.lastSeq !== null) cursor = page.lastSeq;
    if (!page.hasMore) return cursor;
  }
}
```

A page can hold fewer events than `limit`. Continue from `lastSeq` while `hasMore` is true. Filter with `type`, repeated for several.


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