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

# Use SiteGPT from Convex

> Get chatbot answers in Convex functions and keep chatbot content in sync with your Convex tables.

`@sitegpt/convex` is the SiteGPT component for [Convex](https://www.convex.dev) apps. It calls [API v2](/docs/api-reference/v2/getting-started) from your Convex backend. Use it when your app runs on Convex and you want to:

* **Get answers on the server.** Call `sitegpt.ask()` in a Convex action. You get the chatbot's answer back, for example for an "Ask AI" box in your app.
* **Keep content in sync.** Call `sitegpt.syncDocument()` in the same mutation that writes your data. The chatbot's content then follows your Convex table. When you delete the row, the chatbot forgets it.

The component also has typed methods for content, conversations, leads, and account data.

The package is on [npm](https://www.npmjs.com/package/@sitegpt/convex) and in the official [Convex Components directory](https://www.convex.dev/components/sitegpt/convex).

<Note>
  This component runs in your Convex backend. To show the chat widget on your website, see [Add the chatbot to your website](/docs/guides/install/add-to-website).
</Note>

## Before you start

* **Plan:** API access is available on the Growth plan and above. See [Plans and limits](/docs/reference/plans-and-limits).
* **API token:** Create a token on the **Agents** page. Give it the scopes for the methods you use. See [Scopes you need](#scopes-you-need) and [Authentication](/docs/developers/authentication).
* **Convex:** Your app must use `convex` 1.42.0 or later.
* **Chatbot ID:** On your chatbot's **Installation** page, copy the **Chatbot ID**.

## Set up the component

<Steps>
  <Step title="Install the package">
    In your Convex project, run:

    ```sh theme={null}
    npm install @sitegpt/convex
    ```
  </Step>

  <Step title="Register the component">
    In `convex/convex.config.ts`, add the component and pass it the `SITEGPT_API_TOKEN` environment variable:

    ```ts convex/convex.config.ts theme={null}
    import sitegpt from '@sitegpt/convex/convex.config.js'
    import { defineApp } from 'convex/server'
    import { v } from 'convex/values'

    const app = defineApp({
      env: { SITEGPT_API_TOKEN: v.string() },
    })

    app.use(sitegpt, {
      env: { SITEGPT_API_TOKEN: app.env.SITEGPT_API_TOKEN },
    })

    export default app
    ```

    The component is now available as `components.sitegpt`.
  </Step>

  <Step title="Set the API token">
    Store the token on your Convex deployment:

    ```sh theme={null}
    npx convex env set SITEGPT_API_TOKEN YOUR_API_TOKEN
    ```

    The component reads the token only from `SITEGPT_API_TOKEN`. Do not put the token in your code.
  </Step>

  <Step title="Create the client">
    Create a `SiteGPT` client in your `convex/` folder:

    ```ts convex/support.ts theme={null}
    import { SiteGPT } from '@sitegpt/convex'
    import { components } from './_generated/api'

    const sitegpt = new SiteGPT(components.sitegpt, {
      defaultChatbotId: 'YOUR_CHATBOT_ID',
    })
    ```

    `defaultChatbotId` is optional. Every method also takes a `chatbotId` argument. Use it to work with more than one chatbot.
  </Step>
</Steps>

## Ask the chatbot

`ask()` sends a message to the chatbot and returns the answer in the same call. It must run in a Convex action.

```ts convex/support.ts theme={null}
import { SiteGPT } from '@sitegpt/convex'
import { v } from 'convex/values'

import { components } from './_generated/api'
import { action } from './_generated/server'

const sitegpt = new SiteGPT(components.sitegpt, {
  defaultChatbotId: 'YOUR_CHATBOT_ID',
})

export const ask = action({
  args: {
    question: v.string(),
    threadId: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const result = await sitegpt.ask(ctx, {
      message: args.question,
      threadId: args.threadId,
    })
    return { answer: result.answer, threadId: result.threadId }
  },
})
```

* Without a `threadId`, `ask()` starts a new conversation.
* To continue the conversation, pass the `threadId` from the last result.
* The conversation shows in **Chat History** like any other conversation. Your team can read it, take it over, and resolve it there.
* If a person on your team has taken over the conversation, the AI does not answer. `answer` is then `null`. See [Conversations and handoff](/docs/concepts/conversations-and-handoff).
* If the conversation is resolved, `ask()` throws an error with the code `CONVERSATION_CLOSED`.

## Sync content from a Convex table

Call `syncDocument()` in the mutation that saves your data. Call `removeDocument()` in the mutation that deletes it. Give each document a stable `key` that you choose, for example `articles/pricing`.

```ts convex/articles.ts theme={null}
import { SiteGPT } from '@sitegpt/convex'
import { v } from 'convex/values'

import { components } from './_generated/api'
import { mutation, query } from './_generated/server'

const sitegpt = new SiteGPT(components.sitegpt, {
  defaultChatbotId: 'YOUR_CHATBOT_ID',
})

export const save = mutation({
  args: { slug: v.string(), title: v.string(), body: v.string() },
  handler: async (ctx, args) => {
    // Write the article to your own table here.

    await sitegpt.syncDocument(ctx, {
      key: `articles/${args.slug}`,
      name: args.title,
      content: `# ${args.title}\n\n${args.body}`,
    })
  },
})

export const remove = mutation({
  args: { slug: v.string() },
  handler: async (ctx, args) => {
    // Delete the article from your own table here.

    await sitegpt.removeDocument(ctx, { key: `articles/${args.slug}` })
  },
})

export const syncState = query({
  args: { slug: v.string() },
  handler: async (ctx, args) =>
    sitegpt.getSyncState(ctx, { key: `articles/${args.slug}` }),
})
```

How the sync works:

* The sync request is saved in the same transaction as your own write. If your mutation fails, nothing is sent to SiteGPT.
* After the mutation commits, the component sends the change to SiteGPT in the background. The first sync adds a Markdown file to the chatbot's content. Later syncs replace the text of that file. `removeDocument()` deletes it.
* If the content did not change, `syncDocument()` sends nothing. You can call it on every save.
* If someone deleted the file in the dashboard, the next sync with changed content adds it again.
* A failed sync is tried again after 5 seconds, then after 10, 20, and so on, up to 8 attempts. Then the status is `failed`. The next `syncDocument()`, `removeDocument()`, or `retrySync()` for that key starts again.
* Synced documents use pages of your content quota, like uploaded files. See [Plans and limits](/docs/reference/plans-and-limits).
* `getSyncState()` and `listSyncStates()` are Convex queries. Your UI can subscribe to them and show the sync status live.

## Check that it works

1. Ask a question from your terminal:

   ```sh theme={null}
   npx convex run support:ask '{"question": "What are your opening hours?"}'
   ```

   The output has an `answer` and a `threadId`.
2. In the dashboard, open **Chat History**. The new conversation is in the list.
3. Save one row through your sync mutation. Then run your `syncState` query. The `status` changes from `pending` to `synced`, and `documentId` is set.
4. In the dashboard, go to **Files & Data Sources** > **Files List**. The document is in the list. Its file name comes from the `name` you gave it, or from the `key` if you gave no name, and ends in `.md`.

If `status` is `failed`, read `lastError`. See [Errors](#errors).

## Reference

### Client

```ts theme={null}
new SiteGPT(components.sitegpt, options?)
```

| Option             | Type     | Description                                                         |
| ------------------ | -------- | ------------------------------------------------------------------- |
| `defaultChatbotId` | `string` | Optional. The chatbot to use when a call does not pass `chatbotId`. |

If a call has no `chatbotId` and the client has no `defaultChatbotId`, the method throws an error before it sends a request.

Every method takes the Convex `ctx` first and an arguments object second. Every arguments object also accepts an optional `chatbotId`. In the tables below, `?` marks an optional argument.

### Environment variables

| Variable            | Required | Description                                                                                                    |
| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `SITEGPT_API_TOKEN` | Yes      | Your API token. Pass it to the component in `convex/convex.config.ts`.                                         |
| `SITEGPT_API_BASE`  | No       | The API address. The default is `https://sitegpt.ai`. Change it only if SiteGPT gives you a different address. |

To use `SITEGPT_API_BASE`, declare it in `defineApp` with `v.optional(v.string())` and pass it in `app.use()` the same way as the token.

### Chat

Runs in an action.

| Method | Arguments                          | Returns                                       |
| ------ | ---------------------------------- | --------------------------------------------- |
| `ask`  | `message`, `threadId?`, `pageUrl?` | `{ threadId, answer, message, conversation }` |

* `answer` is the answer text, or `null` if the chatbot did not answer.
* `message` is the full message, with the question and the answer.
* `conversation` is the new conversation when the call started one. It is `null` when you passed a `threadId`.
* `pageUrl` must be a full URL. It is saved with the message.

### Content sync

Call `syncDocument`, `removeDocument`, and `retrySync` in a mutation. Call `getSyncState` and `listSyncStates` in a query.

| Method           | Arguments                 | Returns                                                                                        |
| ---------------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
| `syncDocument`   | `key`, `content`, `name?` | `{ status, changed }`. `status` is `pending` or `synced`.                                      |
| `removeDocument` | `key`                     | `{ status, changed }`. `status` is `deleting` or `null`.                                       |
| `retrySync`      | `key`                     | `{ status, changed }`. Starts a `failed` sync again. For other statuses, it changes nothing.   |
| `getSyncState`   | `key`                     | A sync state, or `null` if the key is not synced.                                              |
| `listSyncStates` | `cursor?`, `limit?`       | `{ states, hasNextPage, nextCursor }`. Ordered by key. `limit` is 1 to 100. The default is 50. |

`changed` is `false` when the call did not change anything.

A sync state has these fields:

| Field               | Type             | Description                                                                                                                              |
| ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `key`               | `string`         | Your key.                                                                                                                                |
| `name`              | `string \| null` | The display name.                                                                                                                        |
| `status`            | `string`         | `pending`: waiting to send. `synced`: SiteGPT has the latest content. `deleting`: waiting to delete. `failed`: stopped after 8 attempts. |
| `documentId`        | `string \| null` | The ID of the document in SiteGPT, after the first sync.                                                                                 |
| `contentHash`       | `string`         | A hash of the last content you passed.                                                                                                   |
| `attempts`          | `number`         | Failed attempts since the last change.                                                                                                   |
| `lastError`         | `string \| null` | The last error, for example `CHATBOT_PAGES_LIMIT_REACHED (HTTP 429): ...`.                                                               |
| `updatedAt`         | `number`         | Last change, in milliseconds since the Unix epoch.                                                                                       |
| `nextAttemptAt`     | `number \| null` | When the component tries next, in milliseconds since the Unix epoch.                                                                     |
| `hasPendingContent` | `boolean`        | `true` while the content is not yet sent, and for a `failed` sync that still has content to send.                                        |

### Content

Runs in an action. The arguments match the fields of the API v2 content endpoints. See the [API v2 reference](/docs/api-reference/v2/getting-started).

| Method                  | Arguments                                                                                                                                 | Returns                                                                                                             |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `addLinks`              | `urls`, `syncFrequency?`, `skipExisting?`, `scrapeOptions?`                                                                               | `{ ingestJobRunId, documents, source, requested, effective }`                                                       |
| `addSitemap`            | `url`, `maxLinks?`, `includePaths?`, `excludePaths?`, `syncFrequency?`, `scanFrequency?`, `skipExisting?`, `scrapeOptions?`               | Same as `addLinks`                                                                                                  |
| `crawlWebsite`          | `url`, `maxDepth?`, `maxLinks?`, `includePaths?`, `excludePaths?`, `allowedDomains?`, `syncFrequency?`, `skipExisting?`, `scrapeOptions?` | Same as `addLinks`                                                                                                  |
| `addYoutube`            | `urls`, `syncFrequency?`                                                                                                                  | Same as `addLinks`                                                                                                  |
| `uploadFiles`           | `files`: a list of `{ name, type, base64 }`                                                                                               | `{ ingestJobRunIds, documents, source, requested, effective }`                                                      |
| `setCustomText`         | `text`, `name?`                                                                                                                           | `{ document, replacedDocumentId }`. Replaces the chatbot's one custom text. For many documents, use `syncDocument`. |
| `listDocuments`         | `query?`, `sources?`, `statuses?`, `types?`, `limit?`, `cursor?`                                                                          | `{ documents, pagination }`                                                                                         |
| `getDocument`           | `documentId`, `includeContent?`, `maxContentChars?`                                                                                       | `{ document, ingestJob, content? }`                                                                                 |
| `updateDocumentContent` | `documentId`, `content`                                                                                                                   | `{ document: { id, status } }`                                                                                      |
| `deleteDocument`        | `documentId`                                                                                                                              | `{ itemsDeleted }`                                                                                                  |
| `deleteDocuments`       | `documentIds?`, `state?`, `all?`, `query?`, `sources?`, `statuses?`, `types?`                                                             | `{ itemsDeleted }`                                                                                                  |
| `resyncDocuments`       | Same as `deleteDocuments`                                                                                                                 | `{ itemsQueued, itemsSkipped }`                                                                                     |
| `getDocumentStats`      | `query?`, `sources?`, `statuses?`, `types?`                                                                                               | Counts by source, status, and type, and usage                                                                       |

* `scrapeOptions` has these optional fields: `onlyMainContent`, `includeSelectors`, `excludeSelectors`, and `headers`.
* `state` is `all`, `failed`, `pending`, or `trained`.
* `deleteDocument` and `deleteDocuments` send the delete confirmation for you. They delete at once.
* `pagination` is `{ limit, hasNextPage, nextCursor }`. Pass `nextCursor` as `cursor` to get the next page.

### Conversations and leads

Runs in an action.

| Method              | Arguments                                                                                                                                  | Returns                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| `listConversations` | `status?`, `mode?`, `escalated?`, `important?`, `read?`, `leadId?`, `tagIds?`, `reaction?`, `query?`, `includeEmpty?`, `limit?`, `cursor?` | `{ conversations, filters, pagination }` |
| `getConversation`   | `threadId`                                                                                                                                 | `{ conversation }`                       |
| `listMessages`      | `threadId`, `limit?`, `cursor?`                                                                                                            | `{ messages, pagination }`               |
| `listLeads`         | `status?` (`all`, `archived`, or `open`), `query?`, `important?`, `limit?`, `cursor?`                                                      | `{ leads, pagination }`                  |
| `getLead`           | `leadId`                                                                                                                                   | `{ lead }`                               |

### Account

Runs in an action.

| Method                | Arguments              | Returns                                                                       |
| --------------------- | ---------------------- | ----------------------------------------------------------------------------- |
| `me`                  | None                   | The signed-in user and token details. Use it to test your setup.              |
| `usage`               | None                   | Usage in the current period, compared to your quotas.                         |
| `limits`              | None                   | Plan limits and the limits of each chatbot.                                   |
| `listChatbots`        | None                   | Your chatbots.                                                                |
| `getChatbot`          | `chatbotId?`           | One chatbot.                                                                  |
| `getChatbotAnalytics` | `startDay?`, `endDay?` | Daily engagement numbers with totals and a comparison to the previous period. |

For `getChatbotAnalytics`, write `startDay` and `endDay` as `YYYY-MM-DD` dates in UTC. Without a range, the API uses the last 30 days. If analytics is not on your plan, the call fails with `403` and the code `ANALYTICS_LOCKED`.

The package exports TypeScript types for the return values, for example `AskResult`, `SyncState`, `SiteGptConversation`, and `SiteGptLead`.

To call a component function without the client, use `ctx.runAction()` directly. For example: `ctx.runAction(components.sitegpt.knowledge.addLinks, { chatbotId: 'YOUR_CHATBOT_ID', urls: ['https://example.com/pricing'] })`.

### Scopes you need

| Methods                                                                                                                                                         | Scope                 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `ask`                                                                                                                                                           | `conversations:write` |
| `listConversations`, `getConversation`, `listMessages`                                                                                                          | `conversations:read`  |
| `syncDocument`, `retrySync`, `addLinks`, `addSitemap`, `crawlWebsite`, `addYoutube`, `uploadFiles`, `setCustomText`, `updateDocumentContent`, `resyncDocuments` | `knowledge:write`     |
| `removeDocument`, `deleteDocument`, `deleteDocuments`                                                                                                           | `knowledge:delete`    |
| `listDocuments`, `getDocument`, `getDocumentStats`                                                                                                              | `knowledge:read`      |
| `listLeads`, `getLead`                                                                                                                                          | `leads:read`          |
| `me`, `usage`, `limits`                                                                                                                                         | `account:read`        |
| `listChatbots`, `getChatbot`, `getChatbotAnalytics`                                                                                                             | `chatbots:read`       |

For content sync, give the token both `knowledge:write` and `knowledge:delete`. The sync deletes the old document when you call `removeDocument()`.

## Errors

When the API returns an error, the method throws a `ConvexError`. Its `data` has these fields:

| Field       | Description                                                 |
| ----------- | ----------------------------------------------------------- |
| `kind`      | Always `SiteGptApiError`.                                   |
| `status`    | The HTTP status, for example `403`.                         |
| `code`      | The error code, for example `TOKEN_SCOPE_NOT_ALLOWED`.      |
| `message`   | A readable message.                                         |
| `hint`      | The next step, when the API sends one.                      |
| `requestId` | The request ID, when the API sends one. Give it to support. |

```ts theme={null}
import { ConvexError } from 'convex/values'

try {
  await sitegpt.ask(ctx, { message: 'What are your opening hours?' })
} catch (error) {
  if (error instanceof ConvexError && error.data?.kind === 'SiteGptApiError') {
    console.error(error.data.status, error.data.code, error.data.message)
  }
  throw error
}
```

* `NON_JSON_RESPONSE` and `INVALID_API_RESPONSE` mean that the answer did not come from API v2. Check `SITEGPT_API_BASE`.
* If `SITEGPT_API_TOKEN` is not set, the method throws a `ConvexError` that tells you how to set it.
* `syncDocument()` throws in your mutation when the `key`, `name`, or `content` is too long. Your mutation then fails and saves nothing. See [Limits](#limits).
* Errors during the background sync do not reach your mutation. The component saves them in `lastError` on the sync state.

For the API error codes, see [API conventions](/docs/api-reference/v2/conventions#errors).

## Limits

| Item            | Limit                                                                          |
| --------------- | ------------------------------------------------------------------------------ |
| Sync `key`      | 1 to 512 characters                                                            |
| Sync `name`     | 120 characters                                                                 |
| Sync `content`  | 900,000 bytes of UTF-8 text per document. Split larger content into more keys. |
| Background sync | 10 documents per batch. Deletes go before updates.                             |
| Sync attempts   | 8, then `failed`                                                               |
| `ask` message   | 20,000 characters                                                              |

## Related

* [TypeScript SDK](/docs/developers/sdk-typescript)
* [Authentication](/docs/developers/authentication)
* [API v2 reference](/docs/api-reference/v2/getting-started)
* [Source code and README on GitHub](https://github.com/sitegpt/convex-component)
* [@sitegpt/convex on npm](https://www.npmjs.com/package/@sitegpt/convex)
