
# Knowledge Base API

Your knowledge base is what the AI reads from. It has two parts, and this page covers both:

- **Knowledge sources** (`/kb-sources`) — the web pages and uploaded documents you feed the platform. Each one is read, split into sections, and turned into FAQs your AI can answer from.
- **Knowledge groups** (`/kb-groups`) — named bundles of FAQs you can apply to an Agent or a campaign in a single call, so a body of knowledge you have already curated can be reused on the next Agent you create.

The FAQs a source produces land in the same library as the ones you write by hand, so once an import finishes you read, edit and link them with the [FAQs API](faqs.md).

All endpoints below are relative to the base URL `https://api.youraiconnector.com/v1`. Every request must be authenticated — see [API Access](../integrations/api-access.md) and [Authentication](authentication.md). API access is a paid feature; without it, requests are rejected with a `403`.


> **Importing costs credits.** Reading a page or document and writing FAQs from it consumes credits, roughly in proportion to how much content there is. Use [Estimate an import](#estimate-what-an-import-will-cost) before committing to a large crawl.

---

## How an import works

Importing is a background job, not something that finishes while you wait. Every import endpoint answers immediately with a `source_id`, and you poll that source until it is done:

1. **Start the import** — `POST /kb-sources/url` (one page), `POST /kb-sources/file` (an uploaded document), or `POST /kb-sources/bulk-import` (up to 100 pages). You get back a source ID and `status: "queued"`.
2. **Poll** — `GET /kb-sources/{sourceId}` until `status` is no longer `queued` or `processing`.
3. **Read the FAQs** — when the status is `ready`, the entries it produced are in your FAQ library: `GET /faqs`.

Every source reports one of these statuses:

| Status | What it means |
|---|---|
| `queued` | Waiting to be read. Nothing has been charged yet. |
| `processing` | Being read and turned into FAQs right now. |
| `ready` | Finished. Its FAQs are in your library. |
| `failed` | Could not be imported. `error_message` says why. |
| `cancelled` | Stopped before it was read (see [Stop an import](#stop-an-import)). |
| `paused` | Stopped because your own AI key failed mid-import (see [Resume a paused import](#resume-a-paused-import)). |
| `deleting` | A bulk removal is working through it. |
| `unknown` | The record carries no status. Treat it as not ready. |

> **Attach as you import.** Pass `autoLinkToAgentId` on any import endpoint and the source — plus every FAQ it produces — lands on that Agent's knowledge in the same call, with no follow-up linking step. `autoLinkToCampaignId` does the same for a classic campaign. Linking is best effort: an ID that does not exist, or belongs to another account, is skipped silently and the import still runs, so confirm the link by reading the Agent back.

---

## Import a web page

`POST /kb-sources/url`

Adds one web page to your knowledge base.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `url` | Yes | Full `http` or `https` address of the page. |
| `autoLinkToAgentId` | No | ID of an AI Agent to attach the imported source to. |
| `autoLinkToCampaignId` | No | Legacy. ID of a campaign to attach the imported source to. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/url?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "autoLinkToAgentId": "ag7HkQ2ZpLxR3mNb"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/kb-sources/url", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/pricing",
    autoLinkToAgentId: "ag7HkQ2ZpLxR3mNb",
  }),
});
const { source_id } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/kb-sources/url",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "url": "https://example.com/pricing",
        "autoLinkToAgentId": "ag7HkQ2ZpLxR3mNb",
    },
)
source_id = res.json().get("source_id")
```

**Response** — `202 Accepted`

```json
{
  "success": true,
  "source_id": "kb_src_abc123",
  "status": "queued",
  "batch_id": "batch_9f2a"
}
```

Poll `source_id` with [Check a source](#check-a-source) until the status is `ready` or `failed`.

If the same page is already in your knowledge base, nothing new is queued and you get a `200` instead — and if you asked for an auto-link, the existing source is linked for you anyway:

```json
{
  "success": true,
  "status": "exists",
  "skipped_duplicate": 1
}
```

A missing `url`, or one that is not a valid `http`/`https` address, returns `400`.

---

## Import an uploaded document

`POST /kb-sources/file`

Adds a document that is **already in your account's file storage** as a knowledge source. Supported types: PDF, DOCX, TXT, MD, CSV and XLSX.

> **This endpoint does not carry the file.** There is no multipart upload, no base64 body and no download-from-a-URL: you send the storage location of a file that already exists, and it must live under your own uploads folder (`storage_path` has to begin with `users/{your user id}/uploads/`) or the request is refused with `403`. The dashboard puts files there when you drag them in. If you have no way to place a file there, import a web page with [Import a web page](#import-a-web-page) instead.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `storage_path` | Yes | Where the uploaded file lives. Must start with `users/{your user id}/uploads/`. |
| `filename` | Yes | Original file name including its extension — this is how the file type is detected. |
| `mime_type` | Yes | MIME type of the file, for example `application/pdf`. |
| `autoLinkToAgentId` | No | ID of an AI Agent to attach the document to. |
| `autoLinkToCampaignId` | No | Legacy. ID of a campaign to attach the document to. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/file?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "storage_path": "users/abc123uid/uploads/handbook.pdf",
    "filename": "handbook.pdf",
    "mime_type": "application/pdf",
    "autoLinkToAgentId": "ag7HkQ2ZpLxR3mNb"
  }'
```

**Response** — `202 Accepted`

```json
{
  "success": true,
  "source_id": "kb_src_abc123",
  "status": "queued"
}
```

| Status | When |
|---|---|
| `400` | A required field is missing, or the file is not a type we can read. |
| `403` | `storage_path` is outside your own uploads folder. |

---

## Check a source

`GET /kb-sources/{sourceId}`

The poll that follows every import and refresh. Repeat it until the status is `ready` or `failed`.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/kb-sources/kb_src_abc123?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/kb-sources/kb_src_abc123",
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const source = await res.json();
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/kb-sources/kb_src_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
source = res.json()
```

**Response**

```json
{
  "success": true,
  "source_id": "kb_src_abc123",
  "status": "ready",
  "faq_count": 24,
  "section_count": 31,
  "error_message": null
}
```

| Field | Type | Description |
|---|---|---|
| `status` | string | Where the source is in the pipeline (see the [status table](#how-an-import-works)). |
| `faq_count` | integer | How many FAQs have been generated from this source so far. |
| `section_count` | integer | How many content sections the source was split into. |
| `error_message` | string \| null | Why the import failed, when the status is `failed`. `null` otherwise. |

---

## Delete a source

`DELETE /kb-sources/{sourceId}`

Removes one knowledge source. **By default the FAQs it produced are kept** — add `delete_faqs=true` to remove those as well.

**Query parameters**

| Parameter | Required | Description |
|---|---|---|
| `delete_faqs` | No | Set to `true` to also delete every FAQ this source produced. Defaults to `false`. |

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/kb-sources/kb_src_abc123?delete_faqs=true&apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true,
  "faqs_deleted": 24
}
```

`faqs_deleted` is `0` unless you asked for `delete_faqs=true`.

---

## Import many pages at once

`POST /kb-sources/bulk-import`

Adds up to 100 web pages in one call — the usual follow-up to [Discover pages on a website](#discover-pages-on-a-website) or [Find new pages on a website](#find-new-pages-on-a-website). Pages already in your knowledge base are skipped rather than duplicated (and are still linked to the Agent when you asked for that).

**Request fields**

| Field | Required | Description |
|---|---|---|
| `urls` | Yes | Addresses to import. At least 1, at most 100 per call. |
| `autoLinkToAgentId` | No | ID of an AI Agent to attach every imported page to. |
| `autoLinkToCampaignId` | No | Legacy. ID of a campaign to attach every imported page to. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/bulk-import?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/pricing", "https://example.com/faq"],
    "autoLinkToAgentId": "ag7HkQ2ZpLxR3mNb"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/kb-sources/bulk-import", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    urls: ["https://example.com/pricing", "https://example.com/faq"],
    autoLinkToAgentId: "ag7HkQ2ZpLxR3mNb",
  }),
});
const { queued_source_ids } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/kb-sources/bulk-import",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "urls": ["https://example.com/pricing", "https://example.com/faq"],
        "autoLinkToAgentId": "ag7HkQ2ZpLxR3mNb",
    },
)
queued_source_ids = res.json()["queued_source_ids"]
```

**Response** — `202 Accepted`

```json
{
  "success": true,
  "batch_id": "batch_9f2a",
  "queued": 2,
  "skipped_duplicate": 0,
  "queued_source_ids": ["kb_src_abc123", "kb_src_def456"]
}
```

Poll each ID in `queued_source_ids` with [Check a source](#check-a-source). Sending an empty `urls` array, a non-string entry, or more than 100 entries returns `400`.

---

## Delete many sources at once

`POST /kb-sources/bulk-delete`

Removes up to 2,000 knowledge sources in one call. The removal runs in the background and you get an email when it finishes.

> **Bulk delete always removes the FAQs too.** Unlike [Delete a source](#delete-a-source), which keeps them unless you ask otherwise, this endpoint deletes each source together with the FAQs it produced. There is no option to keep them.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `sourceIds` | Yes | IDs of the sources to remove. At least 1, at most 2,000 per call. |
| `domainLabel` | No | A friendly name for this clean-up. Used only in the completion email. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/bulk-delete?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceIds": ["kb_src_abc123", "kb_src_def456"],
    "domainLabel": "example.com"
  }'
```

**Response** — `202 Accepted`

```json
{
  "success": true,
  "batch_id": "del_batch_31a",
  "queued": 2
}
```

---

## Discover pages on a website

`POST /kb-sources/discover-pages`

Explores a website from one starting address and lists the pages found on the same domain, each with an opinion on whether it is worth importing. **Nothing is imported and nothing is selected for you** — this is the "what is on this site" step you run before deciding what to send to [Import many pages at once](#import-many-pages-at-once).

**Request fields**

| Field | Required | Description |
|---|---|---|
| `url` | Yes | Address to start exploring from, usually the site's home page. |
| `maxPages` | No | Upper bound on how many pages to return. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/discover-pages?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com", "maxPages": 100 }'
```

**Response**

```json
{
  "success": true,
  "source_type": "sitemap",
  "pages": [
    {
      "url": "https://example.com/pricing",
      "title": "Pricing",
      "depth": 1,
      "score": 95,
      "recommendation": "add",
      "reason_key": "core_page"
    }
  ]
}
```

| Field | Type | Description |
|---|---|---|
| `source_type` | string | How the pages were found — `sitemap` (the site's own sitemap) or `link_discovery` (by following links). |
| `url` | string | Full address of the page. |
| `title` | string \| null | Page title, when one could be read. |
| `depth` | integer | How many links away from the starting page this page was found. |
| `score` | integer | How useful the page looks as knowledge, from `0` to `100`. |
| `recommendation` | string | `add` (clearly worth importing, score 90 or above), `maybe` (borderline), or `skip` (content that rarely helps an assistant — changelogs, legal pages, duplicate translations). |
| `reason_key` | string | A stable, machine-readable reason behind the recommendation, for example `core_page`, `changelog_history`, `legal_page` or `locale_duplicate`. |

> **Exploration is best effort.** If the site cannot be read the response is still `200`, with `success: false`, an empty `pages` list and an `error` message. Check `success` before reading `pages`.

A missing `url` returns `400`.

---

## Estimate what an import will cost

`POST /kb-sources/estimate-cost`

Works out how many credits a proposed import would consume, before you commit to it. Pages are fetched and documents are read to measure their size, but nothing is imported and the estimate itself does not spend credits.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `urls` | No | Page addresses you are considering importing. |
| `files` | No | Already-uploaded files you are considering. Each entry needs `storage_path`, `filename` and `mime_type`. |
| `tier` | No | The AI quality tier the import will run on, so the estimate matches what you will actually be charged. Leave it out for the standard rate. |

Send `urls`, `files`, or both.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/estimate-cost?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "urls": ["https://example.com/pricing"] }'
```

**Response**

```json
{
  "success": true,
  "estimates": [
    { "ref": "https://example.com/pricing", "chunks": 7, "credits": 7 }
  ],
  "total_chunks": 7,
  "total_credits": 7
}
```

Each row echoes back the URL or storage path in `ref` so you can match it to your input. A page or file that could not be read still gets a row, counted as one chunk, with an `error` on it.

---

## Stop an import

`POST /kb-sources/cancel-import`

Stops pages that are still waiting in the import queue — the "stop import" button for a crawl that turned out bigger than you expected. Cancelling a waiting page costs nothing, because it has not been read yet.

Pages already being processed are **not** stopped: their work is under way and is charged either way, so they finish. The response reports how many those were.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `host` | No | Only stop waiting pages on this website (for example `docs.example.com`). Leave it out to stop every waiting import on the account. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/cancel-import?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "host": "docs.example.com" }'
```

**Response**

```json
{
  "success": true,
  "cancelled": 412,
  "in_flight": 3
}
```

---

## Resume a paused import

`POST /kb-sources/resume-import`

Restarts an import that was paused because your own AI key stopped working.

> Calling this **is** your consent to finish the import on whichever key is live now — which may mean spending platform credits if your own key is still down.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `host` | No | Only resume paused pages on this website. Leave it out to resume everything paused. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/resume-import?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Response**

```json
{
  "success": true,
  "resumed": 58
}
```

---

## Find new pages on a website

`POST /kb-sources/refresh-domain`

Explores a website you have already imported from and reports only the pages that are **not** in your knowledge base yet, each with the same recommendation as page discovery. Nothing is imported and nothing is changed.

The two follow-ups are deliberately separate calls, so walking away from this one costs nothing:

- import the new pages you want with [Import many pages at once](#import-many-pages-at-once);
- re-read the pages you already have with [Refresh every page on a website](#refresh-every-page-on-a-website).

**Request fields**

| Field | Required | Description |
|---|---|---|
| `baseUrl` | Yes | Any address on the website, or just the host. |
| `maxPages` | No | Upper bound on how many pages to explore. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/refresh-domain?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "baseUrl": "https://example.com" }'
```

**Response**

```json
{
  "success": true,
  "source_type": "sitemap",
  "discovered": 249,
  "new_pages": [
    {
      "url": "https://example.com/new-guide",
      "score": 95,
      "recommendation": "add",
      "reason_key": "core_page"
    }
  ],
  "new_urls_queued": 0,
  "existing_refresh_queued": 249
}
```

| Field | Type | Description |
|---|---|---|
| `discovered` | integer | How many pages were found on the site in total. |
| `new_pages` | array | Pages not in your knowledge base yet. Nothing is queued for you — import the ones you want. |
| `new_urls_queued` | integer | Always `0`. Kept for backwards compatibility; this endpoint never queues anything. |
| `existing_refresh_queued` | integer | How many pages you already imported from this site were found ready to be re-read. Nothing is queued by this call. |
| `batch_id` | string | Present only when a batch was created. |

Like discovery, this fails softly: a site that cannot be read still returns `200`, with `success: false`, an empty `new_pages` and an `error`. A missing or empty `baseUrl` returns `400`.

---

## Refresh every page on a website

`POST /kb-sources/trigger-domain-refresh`

Re-reads every page you have already imported from a website, so its FAQs follow the site's current content: changed sections are updated, new sections added and removed sections dropped.

This queues work and returns immediately. Follow it with [Track a website refresh](#track-a-website-refresh), and stop it with [Stop a website refresh](#stop-a-website-refresh).

**Request fields**

| Field | Required | Description |
|---|---|---|
| `baseUrl` | Yes | Any address on the website, or just the host. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/trigger-domain-refresh?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "baseUrl": "https://example.com" }'
```

**Response**

```json
{
  "success": true,
  "queued": 249
}
```

---

## Track a website refresh

`GET /kb-sources/domain-refresh-status`

How far along a website refresh is, so you can show progress like "221 of 249".

**Query parameters**

| Parameter | Required | Description |
|---|---|---|
| `baseUrl` | Yes | Any address on the website, or just the host. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/kb-sources/domain-refresh-status?baseUrl=https://example.com&apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true,
  "job": {
    "domainBatchId": "job_7c1e",
    "host": "example.com",
    "total": 249,
    "pending": 28,
    "succeeded": 219,
    "failed": 2,
    "skippedDuplicate": 0,
    "status": "refreshing",
    "startedAtIso": "2026-06-15T09:00:00.000Z"
  }
}
```

`job` is `null` when no refresh is running for that website. Pages finished so far is `total` minus `pending`. The job `status` is one of `refreshing` (still working through pages), `deduplicating` (the clean-up pass at the end), or the final `completed`, `failed` and `cancelled`. Keep `domainBatchId` — it is what you pass to the cancel endpoint.

A missing or empty `baseUrl` returns `400`.

---

## Stop a website refresh

`POST /kb-sources/refresh-domain/cancel`

Stops a website refresh that is still working through its pages. Pages already finished keep their updated content; pages not started are dropped, and pages that were being re-read go back to their previous state.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `jobId` | Yes | The `domainBatchId` returned by [Track a website refresh](#track-a-website-refresh). |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/refresh-domain/cancel?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "jobId": "job_7c1e" }'
```

**Response**

```json
{
  "success": true,
  "status": "cancelled",
  "cancelled_units": 28,
  "sources_reset": 3,
  "sources_cancelled": 25
}
```

| Field | Type | Description |
|---|---|---|
| `status` | string | State of the refresh after this call: `cancelled`, `deduplicating`, `completed` or `failed`. |
| `cancelled_units` | integer | How much work was still outstanding when the cancel landed. `0` on a repeat cancel. |
| `sources_reset` | integer | Pages taken back out of processing and returned to `ready`. |
| `sources_cancelled` | integer | Brand-new pages of this refresh that were still queued and are now cancelled. |

Cancelling twice is harmless — the second call reports the same final state. Once the refresh has moved on to its clean-up pass it can no longer be stopped, and the response comes back with `success: false` and `reason: "already_finalizing"`. A missing `jobId` returns `400`, and a job that is not in your account returns `404`.

---

## Refresh a single source

`POST /kb-sources/{sourceId}/refresh`

Re-reads one web page you have already imported and brings its FAQs back in line with the page's current content: changed sections are updated, new ones added, removed ones dropped.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/kb_src_abc123/refresh?apiKey=YOUR_API_KEY"
```

**Response** — `202 Accepted`

```json
{
  "success": true,
  "source_id": "kb_src_abc123",
  "status": "queued"
}
```

Poll the source until its status leaves `queued` and `processing`. A source ID that is not in your account returns `404`.

---

## Pick the most relevant pages

`POST /kb-sources/select-relevant-pages`

Asks the AI to pick the five pages, out of a list of candidates, that best describe a business — used when generating a campaign playbook from a website. This consumes credits.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `urls` | Yes | Candidate page addresses to choose from, usually from page discovery. |
| `homeUrl` | Yes | The site's home page, used as context for the choice. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-sources/select-relevant-pages?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "homeUrl": "https://example.com",
    "urls": ["https://example.com/about", "https://example.com/pricing"]
  }'
```

**Response**

```json
{
  "success": true,
  "pages": [
    { "url": "https://example.com/pricing", "title": "Pricing", "type": "pricing" }
  ]
}
```

This is a helper, not a resource: on failure it still answers `200`, with `success: false`, an empty `pages` list and an `error` message.

---

## Knowledge groups

A **knowledge group** is a named bundle of FAQs — "Shipping and returns", "Onboarding" — that you can apply to an Agent or a campaign in one call. The group holds references, not copies: the FAQs themselves stay in your single library, so editing one with the [FAQs API](faqs.md) updates it everywhere it is used.

Applying a group only ever **adds** what is missing, so applying the same group twice is harmless and `added_count` comes back as `0` the second time.

---

## Create a knowledge group

`POST /kb-groups`

Creates a group. It starts empty — add FAQs to it with [Add a FAQ to a group](#add-a-faq-to-a-group).

**Request fields**

| Field | Required | Description |
|---|---|---|
| `name` | Yes | Name of the group. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-groups?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Shipping and returns" }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/kb-groups", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Shipping and returns" }),
});
const { group_id } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/kb-groups",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"name": "Shipping and returns"},
)
group_id = res.json()["group_id"]
```

**Response** — `201 Created`

```json
{
  "success": true,
  "group_id": "kbg_abc123"
}
```

---

## Rename a knowledge group

`PUT /kb-groups/{groupId}`

Changes a group's name. Its FAQs are untouched.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `name` | Yes | New name for the group. |

**cURL**

```bash
curl -X PUT "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Shipping, returns and refunds" }'
```

**Response**

```json
{
  "success": true,
  "group_id": "kbg_abc123",
  "name": "Shipping, returns and refunds"
}
```

---

## Delete a knowledge group

`DELETE /kb-groups/{groupId}`

Deletes the group. Only the bundle is removed — the FAQs in it stay in your library, and anything the group was already applied to keeps those FAQs.

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true
}
```

---

## Add a FAQ to a group

`POST /kb-groups/{groupId}/faqs`

Puts an existing FAQ into a group. This only changes the bundle — it does not attach the FAQ to any Agent by itself; apply the group for that.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `faq_id` | Yes | ID of the FAQ to add. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/faqs?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "faq_id": "aBcD1234eFgH5678" }'
```

**Response**

```json
{
  "success": true,
  "group_id": "kbg_abc123",
  "faq_id": "aBcD1234eFgH5678"
}
```

---

## Remove a FAQ from a group

`DELETE /kb-groups/{groupId}/faqs/{faqId}`

Takes a FAQ out of a group. The FAQ itself is not deleted, and Agents the group was already applied to keep it.

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/faqs/aBcD1234eFgH5678?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true,
  "group_id": "kbg_abc123",
  "faq_id": "aBcD1234eFgH5678"
}
```

---

## Apply a group to an Agent

`POST /kb-groups/{groupId}/apply-to-agent`

Adds every FAQ in the group to an AI Agent's knowledge in one call — the fast way to give a new Agent a body of knowledge you have already curated.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `agent_id` | Yes | ID of the AI Agent to apply the group to. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/apply-to-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "ag7HkQ2ZpLxR3mNb" }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/apply-to-agent",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ agent_id: "ag7HkQ2ZpLxR3mNb" }),
  }
);
const { added_count } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/apply-to-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"agent_id": "ag7HkQ2ZpLxR3mNb"},
)
added_count = res.json()["added_count"]
```

**Response**

```json
{
  "success": true,
  "group_id": "kbg_abc123",
  "agent_id": "ag7HkQ2ZpLxR3mNb",
  "added_count": 12
}
```

`added_count` is how many FAQs were actually added — `0` when the group is empty or already applied.

---

## Apply a group to a campaign

`POST /kb-groups/{groupId}/apply-to-campaign`

The classic-campaign version of the call above. On an Agent-based account use [Apply a group to an Agent](#apply-a-group-to-an-agent) instead.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `campaign_id` | Yes | ID of the campaign to apply the group to. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/kb-groups/kbg_abc123/apply-to-campaign?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "campaign_id": "campaign123" }'
```

**Response**

```json
{
  "success": true,
  "group_id": "kbg_abc123",
  "campaign_id": "campaign123",
  "added_count": 12
}
```

---

## Knowledge Base API errors

These endpoints return the standard error envelope:

```json
{
  "success": false,
  "error": "Knowledge base source not found."
}
```

| Status | When it happens on a knowledge base endpoint |
|---|---|
| `400` | A required field is missing or invalid — an empty `url`, a missing `baseUrl` or `jobId`, more than 100 URLs in a bulk import, more than 2,000 IDs in a bulk delete, or a file type we cannot read. |
| `402` | Not enough credits to run the import. Top up and try again. |
| `403` | A `storage_path` outside your own uploads folder — or your plan does not include API access. |
| `404` | The source, group, FAQ, Agent, campaign or refresh job was not found — either it does not exist or it belongs to another account. |

> **Soft failures are not errors.** Discovery (`discover-pages`, `refresh-domain`) and the page-picking helper answer `200` with `success: false` and an `error` message when the website cannot be read, rather than failing the request. Always check `success` before reading the data.

The shared codes every endpoint can return — `401`, `403` (your plan does not include API access), `429` (rate limit) and `500` — are listed with retry guidance in [Errors & Pagination](errors-and-pagination.md).

---

## Related

- [FAQs API](faqs.md) — read, edit and link the FAQs your sources produce.
- [Managing FAQs](../ai-automation/faq-management.md) — the same knowledge base in the dashboard.
- [AI Agents](../ai-agents/ai-agents.md) — the Agents you attach sources and groups to.
- [API Access](../integrations/api-access.md) — generate your API key.
- [Authentication](authentication.md) — all the ways to pass your key.
