Log in

Data sources API

Use the data sources API to keep a bot's knowledge in step with your own system, such as a product catalogue, CMS or internal database. You push text to the bot as manual data sources, then sync them so the bot can answer from them.

ℹ️

Before you start

You need the API channel turned on for your bot, its Channel ID and a Secret key. The API channel is on the Standard, Pro and Enterprise plans. See API overview.

How it works

  1. Create a manual data source and keep its id.
  2. Add rows to it. Each row is one document of markdown text, such as one product or one help article.
  3. Sync the data source. The bot only learns new or changed rows after a sync.
  4. When your content changes, update or delete rows, then sync again.

This API only works with manual data sources that belong to the bot the API channel is on. To add websites, feeds or YouTube from code, use the MCP server. To learn about manual data sources in the dashboard, see Add text manually.

Syncing uses storage tokens, and each data source counts towards your plan's data source limit. See Storage tokens and Plans.

Endpoints

All paths start with https://app.chatthing.ai/api/public/channels/{channelId}/2.0. They also work with 1.0 in place of 2.0.

MethodPathWhat it does
POST/data-sourcesCreate a manual data source
POST/data-sources/listList manual data sources
GET/data-sources?dataSourceId=...Get a manual data source
PUT/data-sourcesUpdate a manual data source
DELETE/data-sourcesDelete a manual data source
POST/data-sources/syncSync a data source
POST/data-sources/rowAdd rows
POST/data-sources/row/listList rows
GET/data-sources/row?dataSourceId=...&dataSourceRowId=...Get a row
PUT/data-sources/rowUpdate rows
DELETE/data-sources/rowDelete rows

Every request needs these headers:

Content-Type: application/json
X-API-Secret-Key: {secretKey}

The data source object

FieldTypeDescription
idstringThe data source's id
typestringAlways MANUAL
statestringnot_synced, syncing, synced or sync_error
lastSyncstring or nullWhen it last synced successfully (ISO 8601)
syncIntervalstring or nullHow often it re-syncs automatically: day, week, month or null for never
createdAtstringWhen it was created (ISO 8601)

Create a manual data source

Creates an empty manual data source on the bot. There's no request body.

POST /data-sources
curl --request POST \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}'
const response = await fetch(
  `https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Secret-Key": secretKey,
    },
  },
);
const { dataSource } = await response.json();
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}

response = requests.post(url, headers=headers)
data_source = response.json()["dataSource"]
{
  "success": true,
  "dataSource": {
    "id": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "type": "MANUAL",
    "state": "not_synced",
    "lastSync": null,
    "syncInterval": null,
    "createdAt": "2026-01-27T15:02:51.970Z"
  }
}

If the bot already has as many data sources as your plan allows, you get 429 and "success": false.

List manual data sources

Returns the bot's manual data sources, oldest first, a page at a time.

POST /data-sources/list
FieldTypeDescription
limitintegerOptional. Items per page, from 1 to 100. Default 10
cursorstringOptional. To get the next page, send the pagination.last value from the previous response
curl --request POST \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/list \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{ "limit": 10 }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/list`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({ limit: 10 }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/list"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}

response = requests.post(url, json={"limit": 10}, headers=headers)
{
  "success": true,
  "dataSources": [
    {
      "id": "19d95fbc-8285-40bc-8cc0-3e1fe597b765",
      "type": "MANUAL",
      "state": "synced",
      "lastSync": "2026-01-27T14:39:03.038Z",
      "syncInterval": null,
      "createdAt": "2025-10-31T14:52:19.264Z"
    }
  ],
  "pagination": {
    "total": 1,
    "last": "19d95fbc-8285-40bc-8cc0-3e1fe597b765"
  }
}

pagination.total is the total number of manual data sources on the bot. pagination.last is the id of the last item on this page (null if the page is empty).

Get a manual data source

Returns one manual data source. Use it to check whether a sync has finished.

GET /data-sources?dataSourceId={dataSourceId}
curl --request GET \
  --url 'https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources?dataSourceId={dataSourceId}' \
  --header 'X-API-Secret-Key: {secretKey}'
await fetch(
  `https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources?dataSourceId=${dataSourceId}`,
  { headers: { "X-API-Secret-Key": secretKey } },
);
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources"
headers = {"X-API-Secret-Key": secret_key}

response = requests.get(url, params={"dataSourceId": data_source_id}, headers=headers)

The response is { "success": true, "dataSource": { ... } } with the data source object. You get 404 if it doesn't exist, isn't manual or belongs to another bot.

Update a manual data source

Changes how often a manual data source re-syncs on its own.

PUT /data-sources
FieldTypeDescription
dataSourceIdstringRequired. The data source's id
syncIntervalstring or nullday, week, month, or null to only sync when you ask. Defaults to null if left out
curl --request PUT \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "syncInterval": "week"
  }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources`, {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({
    dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070",
    syncInterval: "week",
  }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}
payload = {
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "syncInterval": "week",
}

response = requests.put(url, json=payload, headers=headers)

The response is { "success": true, "dataSource": { ... } }. The returned object shows the data source as it was before this change, so call Get a manual data source if you need to confirm the new value.

Delete a manual data source

Deletes a manual data source and everything in it. The bot stops using it. This can't be undone.

DELETE /data-sources
FieldTypeDescription
dataSourceIdstringRequired. The data source's id
curl --request DELETE \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{ "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070" }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources`, {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({ dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070" }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}

response = requests.delete(url, json={"dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070"}, headers=headers)

Returns { "success": true }.

Sync a data source

Starts a sync, so the bot learns any rows you've added, changed or deleted. Syncing runs in the background: poll Get a manual data source until state is synced (or sync_error).

POST /data-sources/sync
FieldTypeDescription
dataSourceIdstringRequired. The data source to sync
curl --request POST \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/sync \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{ "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070" }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/sync`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({ dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070" }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/sync"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}

response = requests.post(url, json={"dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070"}, headers=headers)

Returns { "success": true } when the sync has started. You get 429 if the data source is already syncing or your team is out of storage tokens.

You can also trigger a sync from another system without the API key by using a Sync trigger webhook, and get told when a sync finishes with the Sync success and Sync failure webhooks.

Add rows

Adds one or more rows to a manual data source. Each row is one document. Remember to sync afterwards.

POST /data-sources/row
FieldTypeDescription
dataSourceIdstringRequired. The data source to add to
dataSourceRowsarrayRequired. The rows to add
dataSourceRows[].namestringA name for the row that means something to you, such as a file name or product code. The bot isn't trained on it
dataSourceRows[].contentstringThe row's content as markdown (UTF-8). This is what the bot learns
curl --request POST \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/row \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
      { "name": "returns.md", "content": "# Returns\nYou can return any item within 30 days of delivery." },
      { "name": "delivery.md", "content": "# Delivery\nWe deliver to the UK and Ireland in 3 to 5 working days." }
    ]
  }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/row`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({
    dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070",
    dataSourceRows: [
      { name: "returns.md", content: "# Returns\nYou can return any item within 30 days of delivery." },
      { name: "delivery.md", content: "# Delivery\nWe deliver to the UK and Ireland in 3 to 5 working days." },
    ],
  }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/row"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}
payload = {
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
        {"name": "returns.md", "content": "# Returns\nYou can return any item within 30 days of delivery."},
        {"name": "delivery.md", "content": "# Delivery\nWe deliver to the UK and Ireland in 3 to 5 working days."},
    ],
}

response = requests.post(url, json=payload, headers=headers)

Returns { "success": true }. The new row ids aren't returned: use List rows to find them.

List rows

Returns the rows in a manual data source, oldest first, a page at a time.

POST /data-sources/row/list
FieldTypeDescription
dataSourceIdstringRequired. The data source to list
limitintegerOptional. Rows per page, from 1 to 100. Default 10
cursorstringOptional. To get the next page, send the pagination.last value from the previous response
curl --request POST \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/row/list \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "limit": 10
  }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/row/list`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({
    dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070",
    limit: 10,
  }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/row/list"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}
payload = {"dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070", "limit": 10}

response = requests.post(url, json=payload, headers=headers)
{
  "success": true,
  "dataSourceRows": [
    { "id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62", "name": "returns.md" },
    { "id": "4f401205-5f76-46e1-a2b0-9adf66617d81", "name": "delivery.md" }
  ],
  "pagination": {
    "total": 2,
    "last": "4f401205-5f76-46e1-a2b0-9adf66617d81"
  }
}

pagination.total is the total number of rows in the data source.

Get a row

Returns one row, including its content.

GET /data-sources/row?dataSourceId={dataSourceId}&dataSourceRowId={dataSourceRowId}
curl --request GET \
  --url 'https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/row?dataSourceId={dataSourceId}&dataSourceRowId={dataSourceRowId}' \
  --header 'X-API-Secret-Key: {secretKey}'
await fetch(
  `https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/row?dataSourceId=${dataSourceId}&dataSourceRowId=${dataSourceRowId}`,
  { headers: { "X-API-Secret-Key": secretKey } },
);
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/row"
headers = {"X-API-Secret-Key": secret_key}
params = {"dataSourceId": data_source_id, "dataSourceRowId": data_source_row_id}

response = requests.get(url, params=params, headers=headers)
FieldTypeDescription
idstringThe row's id
typestringAlways MANUAL
statestringThe row's sync state: not_synced, syncing, synced or sync_error
lastSyncstring or nullWhen the row last synced (ISO 8601)
lastModifiedstring or nullWhen the row last changed (ISO 8601)
createdAtstringWhen the row was created (ISO 8601)
contentstringThe row's markdown content
{
  "success": true,
  "dataSourceRow": {
    "id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62",
    "type": "MANUAL",
    "state": "synced",
    "lastSync": "2026-01-27T14:39:03.014Z",
    "lastModified": null,
    "createdAt": "2026-01-27T14:30:01.216Z",
    "content": "# Returns\nYou can return any item within 30 days of delivery."
  }
}

Update rows

Replaces the name and content of one or more rows. Remember to sync afterwards.

PUT /data-sources/row
FieldTypeDescription
dataSourceIdstringRequired. The data source the rows belong to
dataSourceRowsarrayRequired. The rows to update
dataSourceRows[].idstringThe row's id
dataSourceRows[].namestringThe row's new name
dataSourceRows[].contentstringThe row's new markdown content. This replaces the old content
curl --request PUT \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/row \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
      {
        "id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62",
        "name": "returns.md",
        "content": "# Returns\nYou can return any item within 60 days of delivery."
      }
    ]
  }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/row`, {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({
    dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070",
    dataSourceRows: [
      {
        id: "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62",
        name: "returns.md",
        content: "# Returns\nYou can return any item within 60 days of delivery.",
      },
    ],
  }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/row"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}
payload = {
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
        {
            "id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62",
            "name": "returns.md",
            "content": "# Returns\nYou can return any item within 60 days of delivery.",
        }
    ],
}

response = requests.put(url, json=payload, headers=headers)

Returns { "success": true }. If any row id doesn't belong to the data source, the request fails with "success": false, and rows before it in the list may already have been updated.

Delete rows

Deletes one or more rows. Remember to sync afterwards so the bot forgets them.

DELETE /data-sources/row
FieldTypeDescription
dataSourceIdstringRequired. The data source the rows belong to
dataSourceRowsarrayRequired. The rows to delete, each as { "id": "..." }
curl --request DELETE \
  --url https://app.chatthing.ai/api/public/channels/{channelId}/2.0/data-sources/row \
  --header 'Content-Type: application/json' \
  --header 'X-API-Secret-Key: {secretKey}' \
  --data '{
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
      { "id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62" },
      { "id": "4f401205-5f76-46e1-a2b0-9adf66617d81" }
    ]
  }'
await fetch(`https://app.chatthing.ai/api/public/channels/${channelId}/2.0/data-sources/row`, {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secretKey,
  },
  body: JSON.stringify({
    dataSourceId: "41559b21-b8ff-44c3-8658-1c9e34273070",
    dataSourceRows: [
      { id: "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62" },
      { id: "4f401205-5f76-46e1-a2b0-9adf66617d81" },
    ],
  }),
});
import requests

url = f"https://app.chatthing.ai/api/public/channels/{channel_id}/2.0/data-sources/row"
headers = {
    "Content-Type": "application/json",
    "X-API-Secret-Key": secret_key,
}
payload = {
    "dataSourceId": "41559b21-b8ff-44c3-8658-1c9e34273070",
    "dataSourceRows": [
        {"id": "c4f85105-6d07-44c0-bdb7-eb0baf7c6f62"},
        {"id": "4f401205-5f76-46e1-a2b0-9adf66617d81"},
    ],
}

response = requests.delete(url, json=payload, headers=headers)

Returns { "success": true }. Row ids that don't exist are skipped.

Errors

CodeWhen
400A required field is missing, or an id isn't a valid UUID
403The secret key doesn't match this channel
404The channel isn't found or is turned off, or the data source doesn't exist, isn't manual or belongs to another bot
429You've reached your plan's data source limit, the data source is already syncing, or your team is out of storage tokens
500Saving a row failed. Try again

Failed requests on most endpoints also return "success": false in the body. See API overview.

  • Add content manually

    Write or paste content directly into Chat Thing to fill gaps in your knowledge, such as FAQs, policies or details that aren't published anywhere.

  • API overview

    Base URL, API keys, authentication headers, versions, error codes and usage limits for the Chat Thing REST API.

Last updated