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

# Sync chat data into your CRM

> Use webhooks and the Agent Chat API to push transcripts, CSAT ratings, and custom properties to your CRM when a conversation ends.

**APIs involved:** [Webhooks](/docs/api/webhooks/), [Agent Chat API](/docs/api/agent-chat/), [Configuration API](/docs/api/configuration/)

This guide uses HubSpot as the example CRM. The pattern — receive a webhook when a chat closes, fetch the full thread, write to your system — applies to any CRM with an API.

## Prerequisites

* An agent access token with scopes `webhooks--all:rw` and `chats--all:read`. Create one in Text under **Settings → API access → Personal access tokens**.
* A Node.js server that can receive HTTP POST requests. The URL must be publicly reachable over HTTPS — Text cannot deliver webhooks to `localhost`.
* A HubSpot private app token with scopes `crm.objects.contacts.write` and `crm.objects.notes.write`. Create one in HubSpot under **Settings → Integrations → Private Apps**.

<Tip>
  Testing locally? Use a tunnel tool like [ngrok](https://ngrok.com) or
  [localtunnel](https://theboroer.github.io/localtunnel-www/) to expose your
  local server over HTTPS. Run `ngrok http 3000` and use the generated URL (e.g.
  `https://abc123.ngrok.io`) as your webhook URL below.
</Tip>

## Step 1: Set up the receiver

Before you register the webhook with Text, stand up the endpoint that will receive it. The code below is the minimal server — Steps 2 and 3 fill in what happens inside the handler.

```js theme={null}
import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/text", async (req, res) => {
  const { action, payload } = req.body;
  if (action !== "chat_deactivated") return res.sendStatus(200);

  // Steps 2 and 3 go here
  res.sendStatus(200);
});

app.listen(3000, () => console.log("Listening on port 3000"));
```

Text expects a `200` response within a few seconds. If your handler takes longer (e.g. waiting on external APIs), respond `200` immediately and process asynchronously.

## Step 2: Register the webhook

Once your server is running and reachable, run this command **once from your terminal** to tell Text where to deliver `chat_deactivated` events:

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/configuration/action/register_webhook" \
  -H "Authorization: Bearer <AGENT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "chat_deactivated",
    "url": "https://your-server.com/webhooks/text",
    "secret_key": "your-secret-key",
    "description": "Sync closed chats to HubSpot"
  }'
```

To verify the webhook was registered or to remove it later, go to **Settings → Integrations → Webhooks** in Text, or call [`list-webhooks`](/docs/api/configuration/v3.6/webhooks/list-webhooks) and [`unregister-webhook`](/docs/api/configuration/v3.6/webhooks/unregister-webhook).

When an agent closes a chat, Text POSTs the following to your URL:

```json theme={null}
{
  "action": "chat_deactivated",
  "payload": {
    "chat_id": "PJ0MRSHTDG",
    "thread_id": "K600PKZON8"
  }
}
```

## Step 3: Fetch the thread

Use `chat_id` and `thread_id` from the webhook to call [`get-chat`](/docs/api/agent-chat/v3.6/chats/get-chat). Passing `thread_id` returns that specific session rather than defaulting to the latest one — important when a customer has had multiple conversations.

```shell theme={null}
curl -X POST "https://api.livechat.com/v3.6/agent/action/get_chat" \
  -H "Authorization: Bearer <AGENT_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": "PJ0MRSHTDG",
    "thread_id": "K600PKZON8"
  }'
```

The response contains everything you need to build a CRM record:

```json theme={null}
{
  "chat": {
    "id": "PJ0MRSHTDG",
    "users": [
      {
        "id": "b7eff798-f8df-4364-8059-649c35c9ed0c",
        "type": "customer",
        "name": "John Doe",
        "email": "john@example.com"
      },
      {
        "id": "agent1@example.com",
        "type": "agent",
        "name": "Support Agent"
      }
    ],
    "thread": {
      "id": "K600PKZON8",
      "events": [
        {
          "id": "Q0Y2U85XPE3",
          "type": "message",
          "text": "Hi, I need help with my order.",
          "author_id": "b7eff798-f8df-4364-8059-649c35c9ed0c",
          "created_at": "2024-01-15T10:30:00.000000Z"
        },
        {
          "id": "Q0Y2U85XPE4",
          "type": "message",
          "text": "Of course! What's your order number?",
          "author_id": "agent1@example.com",
          "created_at": "2024-01-15T10:30:22.000000Z"
        }
      ],
      "statistics": {
        "agent_rating": {
          "rating": "good",
          "comment": "Very helpful, thank you!"
        }
      }
    }
  }
}
```

Events with `type: "message"` are the chat messages. `statistics.agent_rating` holds the CSAT score and comment if the customer left a rating.

## Step 4: Push to HubSpot

Find or create a HubSpot contact from the customer's email, then attach the transcript as a Note on their timeline.

**Find or create the contact**

```js theme={null}
async function upsertContact(email, name) {
  const searchRes = await fetch(
    "https://api.hubapi.com/crm/v3/objects/contacts/search",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filterGroups: [
          {
            filters: [{ propertyName: "email", operator: "EQ", value: email }],
          },
        ],
      }),
    },
  );
  const { results } = await searchRes.json();
  if (results.length > 0) return results[0].id;

  const [firstname, ...rest] = (name ?? email).split(" ");
  const createRes = await fetch(
    "https://api.hubapi.com/crm/v3/objects/contacts",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        properties: { email, firstname, lastname: rest.join(" ") },
      }),
    },
  );
  return (await createRes.json()).id;
}
```

**Build the transcript and attach it as a Note**

```js theme={null}
function buildTranscript(events, users) {
  const nameById = Object.fromEntries(
    users.map((u) => [u.id, u.name ?? u.email]),
  );
  return events
    .filter((e) => e.type === "message")
    .map(
      (e) =>
        `[${e.created_at}] ${nameById[e.author_id] ?? e.author_id}: ${e.text}`,
    )
    .join("\n");
}

async function createNote(contactId, chat) {
  const transcript = buildTranscript(chat.thread.events, chat.users);
  const rating = chat.thread.statistics?.agent_rating;

  const body = [
    `<b>Text chat:</b> ${chat.id}`,
    rating &&
      `<b>Rating:</b> ${rating.rating}${rating.comment ? ` — "${rating.comment}"` : ""}`,
    "",
    "<b>Transcript:</b>",
    `<pre>${transcript}</pre>`,
  ]
    .filter(Boolean)
    .join("<br>");

  await fetch("https://api.hubapi.com/crm/v3/objects/notes", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      properties: {
        hs_note_body: body,
        hs_timestamp: new Date().toISOString(),
      },
      associations: [
        {
          to: { id: contactId },
          types: [
            { associationCategory: "HUBSPOT_DEFINED", associationTypeId: 202 },
          ],
        },
      ],
    }),
  });
}
```

**The complete server**

Putting it all together — paste the helper functions from above into the same file:

```js theme={null}
import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/text", async (req, res) => {
  const { action, payload } = req.body;
  if (action !== "chat_deactivated") return res.sendStatus(200);

  const chatRes = await fetch(
    "https://api.livechat.com/v3.6/agent/action/get_chat",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TEXT_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        chat_id: payload.chat_id,
        thread_id: payload.thread_id,
      }),
    },
  );
  const { chat } = await chatRes.json();

  const customer = chat.users.find((u) => u.type === "customer");
  if (!customer?.email) return res.sendStatus(200);

  const contactId = await upsertContact(customer.email, customer.name);
  await createNote(contactId, chat);

  res.sendStatus(200);
});

app.listen(3000, () => console.log("Listening on port 3000"));
```

After this runs, the contact's HubSpot timeline shows a Note with the transcript and rating:

```
Text chat: PJ0MRSHTDG
Rating: good — "Very helpful, thank you!"

Transcript:
[2024-01-15T10:30:00Z] John Doe: Hi, I need help with my order.
[2024-01-15T10:30:22Z] Support Agent: Of course! What's your order number?
```

## Going further

**Custom properties** let you push structured data beyond the transcript — ticket category, order ID, resolved status. Register a property namespace via the [Configuration API](/docs/api/configuration/), then write values with [`update-chat-properties`](/docs/api/agent-chat/v3.6/properties/update-chat-properties) during or after the chat. Those values appear on the `chat` object in the `get-chat` response.

**Real-time sync**: to push messages as they arrive rather than when the chat ends, subscribe to [`incoming_event`](/docs/api/webhooks/v3.6/#incoming_event) and push each message individually.
