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

# Webhooks

> Subscribe to real-time event notifications from Lofty via HTTP callbacks.

Webhooks deliver real-time HTTP POST notifications to your endpoint when data changes in a Lofty account. Instead of polling the API, register an HTTPS callback URL and Lofty pushes a JSON payload on each subscribed event.

## Subscribe

```bash theme={null}
POST https://api.lofty.com/v1.0/webhook
```

```json theme={null}
{
  "listId": 2,
  "callbackUrl": "https://your-app.example.com/webhooks/lofty",
  "limit": 100
}
```

| Field         | Type    | Required | Description                                  |
| ------------- | ------- | -------- | -------------------------------------------- |
| `listId`      | integer | Yes      | Event type (1–12, see table below)           |
| `callbackUrl` | string  | Yes      | HTTPS endpoint to receive payloads           |
| `limit`       | integer | No       | Max events per batch (default 100, max 5000) |

<Warning>
  The `callbackUrl` must use HTTPS. HTTP endpoints are rejected.
</Warning>

## Manage subscriptions

| Method   | Endpoint                      | Description               |
| -------- | ----------------------------- | ------------------------- |
| `POST`   | `/v1.0/webhook`               | Create a subscription     |
| `GET`    | `/v1.0/webhooks`              | List active subscriptions |
| `DELETE` | `/v1.0/webhook/{subscribeId}` | Delete a subscription     |

## Delivery timing

Events are typically delivered within **1 minute**. During high traffic, delivery may be delayed but will always occur within **5 minutes**.

## Notification recipients

Which subscribed users receive a callback depends on the subscription's delivery mode (`permissionMode`). In both modes, only users in the lead's team who have subscribed to the event type are considered.

### Assignment-based delivery — `permissionMode: 0` (default)

A subscriber receives the callback only for leads assigned to them or that they administer:

* **Hidden leads** — only the assigned agent receives the notification.
* **Other leads** — the subscriber receives it if they are the assigned agent, or a Company Owner / Company Admin. All other subscribers are excluded.

### Ownership-based delivery — `permissionMode: 1`

A subscriber receives the callback for any lead they manage, regardless of who it is assigned to:

* Every assignment-based case above still applies (assigned agent, Company Owner / Company Admin).
* In addition, the subscriber receives the callback for any lead within their **ownership scope** — owned by their team or office, or personally owned by them — even if it is assigned to someone else.
* Hidden leads are still delivered only to users who manage them.

Ownership-based mode is intended for team/account-level integrations that need to cover every lead in their scope, not only assigned leads. Set `permissionMode` when creating the subscription; it defaults to `0`, so existing subscriptions are unaffected.

## Common fields

All payloads include:

| Field    | Type    | Description                      |
| -------- | ------- | -------------------------------- |
| `teamId` | integer | Team ID where the event occurred |
| `listId` | integer | Event type ID (1–12)             |

## Event types

| listId | Event           | Triggers                                                   |
| ------ | --------------- | ---------------------------------------------------------- |
| 1      | Agent Info      | Agent created or updated                                   |
| 2      | Lead Info       | Lead created, updated, or deleted                          |
| 3      | Lead Activity   | Lead site activity (browse, favorite, search)              |
| 4      | Listing Alert   | Listing alert changed                                      |
| 5      | Transaction     | Transaction created, updated, or deleted                   |
| 6      | Call            | Call event (MANUAL and LOGGED only, excludes AUTO)         |
| 7      | Email           | Email event (MANUAL and LOGGED only, excludes AUTO)        |
| 8      | Text            | Text message event (MANUAL and LOGGED only, excludes AUTO) |
| 9      | Note            | Note created, updated, or deleted                          |
| 10     | Task            | Task created, updated, finished, or deleted                |
| 11     | Appointment     | Appointment created, updated, finished, or deleted         |
| 12     | Pipeline Change | Lead pipeline stage changed                                |

<Warning>
  **Call, Email, and Text webhooks do not fire for AUTO communications.** Only agent-initiated (`MANUAL`) and after-the-fact logged (`LOGGED`) events trigger webhooks. Automated messages sent by Lofty (drip campaigns, auto-responders, Smart Plans, etc.) are excluded. If your integration depends on tracking all communications, you must also poll the Communication API for `AUTO` type events.
</Warning>

***

## Event payloads

### 1. Agent Info

Triggered when an agent is created or updated.

```json theme={null}
{
  "teamId": 123456,
  "listId": 1,
  "updatedAgent": [
    {
      "agentUserId": 789,
      "updateTime": 1736935800000
    }
  ]
}
```

| Field                        | Type    | Description                      |
| ---------------------------- | ------- | -------------------------------- |
| `updatedAgent`               | array   | List of updated agent records    |
| `updatedAgent[].agentUserId` | integer | User ID of the updated agent     |
| `updatedAgent[].updateTime`  | integer | Epoch milliseconds of the update |

### 2. Lead Info

Triggered when a lead is created, updated, or deleted. Only the relevant action fields are present.

```json theme={null}
{
  "teamId": 123456,
  "listId": 2,
  "createdLead": [
    { "leadId": 456, "updateTime": 1736935800000 }
  ],
  "updatedLead": [
    { "leadId": 457, "updateTime": 1736935860000 }
  ],
  "deletedLead": [
    { "leadId": 458, "updateTime": 1736935920000 }
  ]
}
```

| Field           | Type    | Description                       |
| --------------- | ------- | --------------------------------- |
| `createdLead`   | array   | Created leads (present on create) |
| `updatedLead`   | array   | Updated leads (present on update) |
| `deletedLead`   | array   | Deleted leads (present on delete) |
| `[].leadId`     | integer | Lead ID                           |
| `[].updateTime` | integer | Epoch milliseconds of the change  |

### 3. Lead Activity

Triggered when a lead performs site activity (SiteBrowse, SiteFavorite, SiteSearch, etc.).

```json theme={null}
{
  "teamId": 123456,
  "listId": 3,
  "updatedLead": [
    { "leadId": 456, "updateTime": 1736935800000 }
  ]
}
```

| Field                      | Type    | Description                        |
| -------------------------- | ------- | ---------------------------------- |
| `updatedLead`              | array   | Leads with new activity            |
| `updatedLead[].leadId`     | integer | Lead ID                            |
| `updatedLead[].updateTime` | integer | Epoch milliseconds of the activity |

### 4. Listing Alert

Triggered when a listing alert changes.

```json theme={null}
{
  "teamId": 123456,
  "listId": 4,
  "alertId": 789
}
```

| Field     | Type    | Description             |
| --------- | ------- | ----------------------- |
| `alertId` | integer | ID of the changed alert |

### 5. Transaction

Triggered when a transaction is created, updated, or deleted. Only the relevant action fields are present.

```json theme={null}
{
  "teamId": 123456,
  "listId": 5,
  "createdTransaction": [
    {
      "leadId": 456,
      "transactionId": 789,
      "updateTime": 1736935800000
    }
  ],
  "updatedTransaction": [
    {
      "leadId": 457,
      "transactionId": 790,
      "updateTime": 1736935860000,
      "updatedField": { "fieldName": "newValue" },
      "updatedFields": [{ "fieldName": "newValue" }]
    }
  ],
  "deletedTransaction": [
    {
      "leadId": 458,
      "transactionId": 791,
      "updateTime": 1736935920000
    }
  ]
}
```

| Field                | Type    | Description                                      |
| -------------------- | ------- | ------------------------------------------------ |
| `createdTransaction` | array   | Created transactions (present on create)         |
| `updatedTransaction` | array   | Updated transactions (present on update)         |
| `deletedTransaction` | array   | Deleted transactions (present on delete)         |
| `[].leadId`          | integer | Associated lead ID                               |
| `[].transactionId`   | integer | Transaction ID                                   |
| `[].updateTime`      | integer | Epoch milliseconds of the change                 |
| `[].updatedField`    | object  | Changed field key-value pair (nullable)          |
| `[].updatedFields`   | array   | List of changed field key-value pairs (nullable) |

### 6. Call

Triggered on a call event.

```json theme={null}
{
  "teamId": 123456,
  "listId": 6,
  "createdCall": [
    {
      "callId": 101,
      "timelineId": 9876,
      "leadId": 456,
      "updateTime": 1736935800000,
      "agentId": 789,
      "communicationType": "MANUAL"
    }
  ]
}
```

| Field                  | Type    | Description                                                                                                                                                                                                    |
| ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createdCall`          | array   | Call event records                                                                                                                                                                                             |
| `[].callId`            | integer | Dialer call record ID (`MANUAL`) or timeline ID (`LOGGED`). Kept for backward compatibility — prefer `timelineId` for new integrations.                                                                        |
| `[].timelineId`        | integer | Lead timeline row ID for this call event. Always `lead_timeline.id` regardless of `communicationType`. Pass as `communicationId` to `GET /v2.0/communication/call/{communicationId}` to fetch the full record. |
| `[].leadId`            | integer | Associated lead ID                                                                                                                                                                                             |
| `[].updateTime`        | integer | Epoch milliseconds of the call                                                                                                                                                                                 |
| `[].agentId`           | integer | Agent user ID                                                                                                                                                                                                  |
| `[].communicationType` | string  | `MANUAL` or `LOGGED`                                                                                                                                                                                           |

### 7. Email

Triggered on an email event.

```json theme={null}
{
  "teamId": 123456,
  "listId": 7,
  "createdEmail": [
    {
      "emailId": 101,
      "timelineId": 9876,
      "leadId": 456,
      "updateTime": 1736935800000,
      "agentId": 789,
      "communicationType": "MANUAL"
    }
  ]
}
```

| Field                  | Type    | Description                                                                                                                                                                                                      |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createdEmail`         | array   | Email event records                                                                                                                                                                                              |
| `[].emailId`           | integer | Email record ID (`MANUAL`) or timeline ID (`LOGGED`). Kept for backward compatibility — prefer `timelineId` for new integrations.                                                                                |
| `[].timelineId`        | integer | Lead timeline row ID for this email event. Always `lead_timeline.id` regardless of `communicationType`. Pass as `communicationId` to `GET /v2.0/communication/email/{communicationId}` to fetch the full record. |
| `[].leadId`            | integer | Associated lead ID                                                                                                                                                                                               |
| `[].updateTime`        | integer | Epoch milliseconds of the event                                                                                                                                                                                  |
| `[].agentId`           | integer | Agent user ID                                                                                                                                                                                                    |
| `[].communicationType` | string  | `MANUAL` or `LOGGED`                                                                                                                                                                                             |

### 8. Text

Triggered on a text/SMS event.

```json theme={null}
{
  "teamId": 123456,
  "listId": 8,
  "createdText": [
    {
      "textId": 101,
      "timelineId": 9876,
      "leadId": 456,
      "updateTime": 1736935800000,
      "agentId": 789,
      "communicationType": "MANUAL"
    }
  ]
}
```

| Field                  | Type    | Description                                                                                                                                                                                                    |
| ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createdText`          | array   | Text message event records                                                                                                                                                                                     |
| `[].textId`            | integer | SMS record ID (`MANUAL`) or timeline ID (`LOGGED`). Kept for backward compatibility — prefer `timelineId` for new integrations.                                                                                |
| `[].timelineId`        | integer | Lead timeline row ID for this text event. Always `lead_timeline.id` regardless of `communicationType`. Pass as `communicationId` to `GET /v2.0/communication/text/{communicationId}` to fetch the full record. |
| `[].leadId`            | integer | Associated lead ID                                                                                                                                                                                             |
| `[].updateTime`        | integer | Epoch milliseconds of the event                                                                                                                                                                                |
| `[].agentId`           | integer | Agent user ID                                                                                                                                                                                                  |
| `[].communicationType` | string  | `MANUAL` or `LOGGED`                                                                                                                                                                                           |

<Note>
  **communicationType values** — `MANUAL`: agent-initiated communication. `LOGGED`: communication recorded after the fact. `AUTO` events are excluded from webhooks.
</Note>

### 9. Note

Triggered when a note is created, updated, or deleted. Only the relevant action fields are present.

```json theme={null}
{
  "teamId": 123456,
  "listId": 9,
  "createdNote": [
    {
      "noteId": 101,
      "leadId": 456,
      "updateTime": 1736935800000,
      "noteType": "Manual",
      "agentId": 789
    }
  ],
  "updatedNote": {
    "noteId": 102,
    "leadId": 457,
    "updateTime": 1736935860000,
    "noteType": "Manual",
    "agentId": 789
  },
  "deletedNote": {
    "noteId": 103,
    "leadId": 458,
    "updateTime": 1736935920000,
    "noteType": "Manual",
    "agentId": 789
  }
}
```

| Field           | Type    | Description                       |
| --------------- | ------- | --------------------------------- |
| `createdNote`   | array   | Created notes (present on create) |
| `updatedNote`   | object  | Updated note (present on update)  |
| `deletedNote`   | object  | Deleted note (present on delete)  |
| `[].noteId`     | integer | Note ID                           |
| `[].leadId`     | integer | Associated lead ID                |
| `[].updateTime` | integer | Epoch milliseconds of the change  |
| `[].noteType`   | string  | `Manual`, `System`, or `AI`       |
| `[].agentId`    | integer | Agent user ID                     |

### 10. Task

Triggered when a task is created, updated, finished, or deleted. Multiple action fields may appear in a single payload.

```json theme={null}
{
  "teamId": 123456,
  "listId": 10,
  "createdTask": [
    { "taskId": 101, "leadId": 456, "updateTime": 1736935800000 }
  ],
  "updatedTask": [
    { "taskId": 102, "leadId": 457, "updateTime": 1736935860000 }
  ],
  "finishedTask": [
    { "taskId": 103, "leadId": 458, "updateTime": 1736935920000 }
  ],
  "deletedTask": [
    { "taskId": 104, "leadId": 459, "updateTime": 1736935980000 }
  ]
}
```

| Field           | Type    | Description                        |
| --------------- | ------- | ---------------------------------- |
| `createdTask`   | array   | Created tasks (present on create)  |
| `updatedTask`   | array   | Updated tasks (present on update)  |
| `finishedTask`  | array   | Finished tasks (present on finish) |
| `deletedTask`   | array   | Deleted tasks (present on delete)  |
| `[].taskId`     | integer | Task ID                            |
| `[].leadId`     | integer | Associated lead ID                 |
| `[].updateTime` | integer | Epoch milliseconds of the change   |

### 11. Appointment

Triggered when an appointment is created, updated, finished, or deleted. Multiple action fields may appear in a single payload.

```json theme={null}
{
  "teamId": 123456,
  "listId": 11,
  "createdAppt": [
    { "apptId": 101, "leadId": 456, "updateTime": 1736935800000 }
  ],
  "updatedAppt": [
    { "apptId": 102, "leadId": 457, "updateTime": 1736935860000 }
  ],
  "finishedAppt": [
    { "apptId": 103, "leadId": 458, "updateTime": 1736935920000 }
  ],
  "deletedAppt": [
    { "apptId": 104, "leadId": 459, "updateTime": 1736935980000 }
  ]
}
```

| Field           | Type    | Description                               |
| --------------- | ------- | ----------------------------------------- |
| `createdAppt`   | array   | Created appointments (present on create)  |
| `updatedAppt`   | array   | Updated appointments (present on update)  |
| `finishedAppt`  | array   | Finished appointments (present on finish) |
| `deletedAppt`   | array   | Deleted appointments (present on delete)  |
| `[].apptId`     | integer | Appointment ID                            |
| `[].leadId`     | integer | Associated lead ID                        |
| `[].updateTime` | integer | Epoch milliseconds of the change          |

### 12. Pipeline Change

Triggered when a lead's pipeline stage changes.

```json theme={null}
{
  "teamId": 123456,
  "listId": 12,
  "updatedLeadstage": [
    {
      "leadId": 456,
      "updateTime": 1736935800000,
      "changeInfo": {
        "oldstage": "New Lead",
        "newstage": "Contacted"
      }
    }
  ]
}
```

| Field                    | Type    | Description                            |
| ------------------------ | ------- | -------------------------------------- |
| `updatedLeadstage`       | array   | Pipeline stage change records          |
| `[].leadId`              | integer | Lead ID                                |
| `[].updateTime`          | integer | Epoch milliseconds of the stage change |
| `[].changeInfo.oldstage` | string  | Previous pipeline stage name           |
| `[].changeInfo.newstage` | string  | New pipeline stage name                |

***

## Best practices

**Respond immediately.** Return `200 OK` before processing. Move logic to a background job. Lofty may retry if no `200` is received.

**Handle duplicates.** The same event may be delivered more than once. Use event IDs and timestamps for idempotency.

**Validate payloads.** Check that `listId` matches your expected event type before processing.

**Use HTTPS.** Plain HTTP endpoints are rejected.

<Tip>
  Use [ngrok](https://ngrok.com) during development to tunnel webhooks to your local machine.
</Tip>

## Example receiver

```javascript theme={null}
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/lofty', (req, res) => {
  res.sendStatus(200);
  const { listId, events } = req.body;
  processEvents(listId, events).catch(console.error);
});

async function processEvents(listId, events) {
  for (const event of events) {
    console.log(`Event type ${listId}:`, event);
  }
}

app.listen(3000);
```

## Resolving communication events to full records

After receiving a Call / Email / Text webhook, fetch the full record by passing the event's `timelineId` as the `communicationId` path parameter of the v2 by-ID endpoint:

| Webhook event | Lookup endpoint                                   | Response        |
| ------------- | ------------------------------------------------- | --------------- |
| Call          | `GET /v2.0/communication/call/{communicationId}`  | `CallResponse`  |
| Email         | `GET /v2.0/communication/email/{communicationId}` | `EmailResponse` |
| Text          | `GET /v2.0/communication/text/{communicationId}`  | `TextResponse`  |

`timelineId` is the recommended round-trip key — it always points to the same `lead_timeline` row regardless of whether the event is `MANUAL` or `LOGGED`. The legacy `callId` / `emailId` / `textId` fields still ship for backward compatibility, but their underlying record changes between the two `communicationType` values, so they are not safe to pass to the v2 by-ID endpoints.

<Note>
  All IDs in webhook payloads are 64-bit integers. JavaScript clients must handle them carefully — see [JS / TS Integration](/javascript-integration).
</Note>
