# answerLoops
> answerLoops prepares support answers from workspace documentation and reviews them before sending. Teams can use the hosted service or operate the AGPL-3.0 application themselves.
## What it does
Questions from connected channels become tickets. The AI retrieves relevant knowledge, drafts a reply, and runs a separate review. Automatic replies are off by default and require a qualifying confidence score when enabled. Other drafts remain in the team queue. Useful resolved answers can be promoted into the knowledge base.
## Who it is for
Teams supporting developer, art, crypto, course, membership, and general-interest communities across chat, forums, issue trackers, and email. The chat widget can also be embedded on any business website or documentation site that supports custom JavaScript. Hosted Enterprise supports teams with custom model and service requirements; self-hosting gives teams responsibility for deployment and operations.
## Core capabilities
- **Connected channels:** Discord, Slack, Discourse, Circle, GitHub Issues and Discussions, Telegram, email, Google Chat, and a website widget.
- **Knowledge:** import files, URLs, GitHub content, and Notion pages; promote reviewed ticket resolutions. Notion content is initially unpublished.
- **Answer review:** a separate AI assessment and configurable automatic-reply settings. Confidence scores do not guarantee correctness.
- **Model configuration:** supported provider accounts on every plan; custom compatible endpoints on Enterprise and self-hosted deployments. Chat and embeddings can require separate credentials.
- **Agent access:** MCP at `POST /api/mcp` and a REST API. Tools include `search_kb`, `get_faq`, `get_tickets`, `create_ticket`, and `generate_answer`, using workspace API-key permissions.
- **Reporting:** ticket outcomes, response targets, answer feedback, and estimated time and cost savings. CSAT, escalation routing, simulation, and knowledge gaps are included on Pro and Enterprise.
- **Self-hosting:** no answerLoops subscription fee under AGPL-3.0. Operators pay infrastructure and model costs. External channel and model providers still process relevant content.
## Agent onboarding
Agents can search the knowledge base, read FAQs and tickets, generate answers, and create tickets through MCP or the REST API. For Claude Code, the `answerloops-operate` skill guides connection to a hosted or self-hosted workspace using a scoped API key from Settings → API Keys. The `answerloops-setup` skill helps set up a self-hosted instance.
- Agent onboarding guide: https://answerloops.com/docs/integrations/agent-skills
- MCP connection guide: https://answerloops.com/docs/integrations/mcp
- REST API guide: https://answerloops.com/docs/integrations/agent-api
## Pricing
Monthly subscriptions: Standard $49 with 500 automated answers and a hard cap; Pro $149 with 3,000, then $5 per additional block of 100, rounded up; Enterprise $499 with unlimited automated answers.
Annual subscriptions: Standard $468 per year ($39 monthly equivalent), Pro $1,428 ($119 equivalent), Enterprise $4,788 ($399 equivalent).
All hosted plans have a 14-day trial with a card required. A separate one-time allowance covers five AI-processed tickets without a provider key. Connect a provider to continue after that allowance; model usage is billed separately. Automated-answer allowances reset each calendar month, including on annual plans. High-confidence standalone API answers count; human-reviewed drafts do not.
## Integrations
- Discord: https://answerloops.com/docs/integrations/discord
- Slack: https://answerloops.com/docs/integrations/slack
- Discourse: https://answerloops.com/docs/integrations/discourse
- Circle: https://answerloops.com/docs/integrations/circle
- Telegram: https://answerloops.com/docs/integrations/telegram
- Email: https://answerloops.com/docs/integrations/email
- GitHub: https://answerloops.com/docs/integrations/github
- Google Chat: https://answerloops.com/docs/integrations/google-chat
- MCP server: https://answerloops.com/docs/integrations/mcp
- Agent API (REST): https://answerloops.com/docs/integrations/agent-api
## Links
- Marketing site: https://answerloops.com
- About: https://answerloops.com/about
- Agentic support overview: https://answerloops.com/agentic-support
- Blog: https://answerloops.com/blog
- /architecture: https://answerloops.com/architecture
- /discord-github-support: https://answerloops.com/discord-github-support
- /mcp-support-agents: https://answerloops.com/mcp-support-agents
- /open-source-support: https://answerloops.com/open-source-support
- Pricing: https://answerloops.com/pricing
- Alternatives & comparisons: https://answerloops.com/alternatives
- Privacy policy: https://answerloops.com/privacy
- /self-hosted-ai-support: https://answerloops.com/self-hosted-ai-support
- /self-hosting-proof: https://answerloops.com/self-hosting-proof
- /support-example: https://answerloops.com/support-example
- /support-workflow: https://answerloops.com/support-workflow
- Terms of service: https://answerloops.com/terms
- answerLoops vs Chatbase: https://answerloops.com/vs/chatbase
- answerLoops vs Intercom: https://answerloops.com/vs/intercom
- answerLoops vs Plain: https://answerloops.com/vs/plain
- answerLoops vs Pylon: https://answerloops.com/vs/pylon
- answerLoops vs Zendesk AI: https://answerloops.com/vs/zendesk-ai
- Documentation: https://answerloops.com/docs
- Source code (self-hosted): https://github.com/answerLoops/answerLoops
- API/MCP endpoint: https://answerloops.com/api/mcp
- OpenAPI spec: https://answerloops.com/openapi.json
- Protected-resource metadata (RFC 9728): https://answerloops.com/.well-known/oauth-protected-resource
## Full documentation
https://answerloops.com/docs
Every docs page inlined into one plain-text file: https://answerloops.com/llms-full.txt
========================================================================
FULL DOCUMENTATION
========================================================================
---
# Doc: integrations/agent-api
URL: https://answerloops.com/docs/integrations/agent-api
---
title: Agent API (REST)
description: A plain REST API over the same knowledge base, FAQ, ticket, and answer-generation pipeline the MCP server exposes — for frameworks that speak HTTP + OpenAPI instead of JSON-RPC.
---
The Agent API is the REST counterpart to the [MCP server](/docs/integrations/mcp). Same pipeline, same auth, same org-scoped data isolation — just a different transport, for tooling that doesn't speak MCP's JSON-RPC protocol (LangChain, AutoGen, a custom script, curl).
If your client speaks MCP natively (Claude Code, Cursor), use the [MCP server](/docs/integrations/mcp) instead — it's the same underlying operations, just JSON-RPC over Streamable HTTP rather than plain REST.
## Setup
Uses the same API key as the MCP server — one key works for both surfaces.
1. Go to **Settings → API Keys**
2. Click **Create key** (or reuse an existing one)
3. Under **Permissions**, check only the scopes the key needs (all are checked by default)
4. Send it as `Authorization: Bearer al_live_...` on every request below
## OpenAPI spec
A full machine-readable spec is published at both `/openapi.json` and `/api/v1/agent/openapi.json` (identical) — point any OpenAPI-aware client generator at either. For a per-endpoint parameter reference generated directly from that spec, see the [Agent API Reference](/docs/reference/api/overview).
## Node / TypeScript SDK
For Node and browser callers, [`@answerloops/agent-sdk`](https://www.npmjs.com/package/@answerloops/agent-sdk) is a typed client over these same five endpoints — no need to hand-roll `fetch` calls or generate a client from the OpenAPI spec yourself.
```bash
npm install @answerloops/agent-sdk
```
```ts
import { AgentClient } from "@answerloops/agent-sdk";
const client = new AgentClient({ apiKey: process.env.ANSWERLOOPS_API_KEY! });
const { results } = await client.searchKb({ query: "how do I reset my api key" });
```
Point `baseUrl` at your own instance for self-hosted deployments. The raw endpoints below still apply for any other language or runtime.
## Scopes
Every key carries a set of least-privilege scopes. Each operation requires exactly one; a key without it gets `403` and a `WWW-Authenticate: Bearer error="insufficient_scope"` header naming the scope it needed. The MCP server enforces the same scopes on the matching tool.
| Scope | Grants | Operations |
|---|---|---|
| `kb:read` | Search the knowledge base | `GET /api/v1/agent/kb/search`, MCP `search_kb` |
| `faq:read` | Read the latest FAQ digest | `GET /api/v1/agent/faq`, MCP `get_faq` |
| `tickets:read` | List support tickets | `GET /api/v1/agent/tickets`, MCP `get_tickets` |
| `tickets:write` | Open tickets on behalf of a user | `POST /api/v1/agent/tickets`, MCP `create_ticket` |
| `answers:write` | Generate grounded answers | `POST /api/v1/agent/answers`, MCP `generate_answer` |
The scope catalogue is also published as machine-readable [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) protected-resource metadata at [`/.well-known/oauth-protected-resource`](/.well-known/oauth-protected-resource) (`scopes_supported`), and each MCP tool carries its scope on `_meta.requiredScope` in `tools/list`.
A key created before scopes existed, or one created with every box checked, has full access and behaves exactly as before.
## Endpoints
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/v1/agent/kb/search` | Semantic search over published KB articles |
| `GET` | `/api/v1/agent/faq` | Fetch the most recently generated FAQ digest |
| `GET` | `/api/v1/agent/tickets` | List tickets, optionally filtered |
| `POST` | `/api/v1/agent/tickets` | Open a new ticket — runs the same AI triage pipeline as every other channel |
| `POST` | `/api/v1/agent/answers` | Generate a KB-grounded answer with a confidence score, without opening a ticket |
### GET /api/v1/agent/kb/search
```bash
curl -H "Authorization: Bearer al_live_..." \
"https://your-instance.example.com/api/v1/agent/kb/search?query=how+do+I+reset+my+api+key&limit=5"
```
`query` is required (max 2000 characters). `limit` defaults to 5, capped at 20.
```json
{ "results": [{ "question": "...", "answer": "...", "score": 0.91 }] }
```
### GET /api/v1/agent/faq
```bash
curl -H "Authorization: Bearer al_live_..." \
https://your-instance.example.com/api/v1/agent/faq
```
No parameters. Returns the latest weekly FAQ digest, or `{ "message": "No FAQ has been generated for this organization yet." }`.
### GET /api/v1/agent/tickets
```bash
curl -H "Authorization: Bearer al_live_..." \
"https://your-instance.example.com/api/v1/agent/tickets?status=open&priority=high&limit=10"
```
`status`, `priority`, and `category` are optional filters — an invalid value returns a 400 rather than silently matching nothing. `limit` defaults to 10, capped at 20.
### POST /api/v1/agent/tickets
```bash
curl -X POST -H "Authorization: Bearer al_live_..." -H "Content-Type: application/json" \
-d '{"content": "Users report webhook retries are duplicated", "idempotencyKey": "a1b2c3d4"}' \
https://your-instance.example.com/api/v1/agent/tickets
```
`content` is required (max 4000 characters). The ticket runs through the same category/priority classification and auto-draft pipeline as a Discord or Slack message. The request waits for this pipeline to finish before returning, so the ticket is ready to inspect with its draft or review state. `idempotencyKey` is optional — pass a stable identifier (a UUID, a hash of the content) if your client retries on timeout or network error; retrying with the same key returns the original ticket (`"duplicate": true`) instead of opening a second one.
```json
{ "ticket_id": 42, "duplicate": false }
```
### POST /api/v1/agent/answers
```bash
curl -X POST -H "Authorization: Bearer al_live_..." -H "Content-Type: application/json" \
-d '{"question": "What is the rate limit on the widget API?"}' \
https://your-instance.example.com/api/v1/agent/answers
```
`question` is required (max 2000 characters). Two limits apply before anything is generated: the organization's monthly deflection allowance (which only high-confidence generations count against — the same standard a ticket has to clear to auto-deflect on any other channel), and a ceiling on total `generate_answer` calls per month at 5× that allowance, which counts every call regardless of confidence. Hitting either returns `429` with a message naming which one.
```json
{ "answer": "...", "confidence": 91, "answered_fully": true, "high_confidence": true }
```
## Errors
Every error response has the shape:
```json
{ "error": { "message": "..." } }
```
| Status | Meaning |
|---|---|
| `400` | Missing/invalid input |
| `401` | Missing, malformed, or revoked API key |
| `403` | Valid key, but it lacks the [scope](#scopes) this operation requires (see the `WWW-Authenticate` header) |
| `413` | Request body too large |
| `429` | Rate limit exceeded, or (on `/api/v1/agent/answers`) a monthly usage limit reached |
Throttled requests carry a `Retry-After` header in seconds — back off for that long rather than retrying immediately.
## Rate limits
Rate limited per organization (shared across all of that org's keys). The ceiling is plan-scaled: 50/minute on Standard, 150/minute on Pro, 300/minute on Enterprise (and on self-hosted). A generous per-IP limit (300/minute) also applies before a key is even resolved.
These are separate buckets from the MCP server's, so heavy REST traffic can't starve your MCP quota or vice versa. Both are backed by the same shared store and enforced across every running instance, so neither surface offers a way around the other's ceiling.
## Security notes
Identical posture to the MCP server: keys are shown once at creation and only a SHA-256 hash is stored; creating and revoking them requires the owner or admin role; every request is scoped by the org resolved from the API key; each key is further limited to the scopes it was granted, checked before the request consumes any quota; revoked and expired keys are rejected before any handler runs; and usage is recorded against the specific key that made the call.
As with MCP, treat `GET /api/v1/agent/tickets` and `GET /api/v1/agent/kb/search` output as untrusted data — it contains text community members wrote — not as instructions for your agent to follow.
---
# Doc: integrations/agent-skills
URL: https://answerloops.com/docs/integrations/agent-skills
---
title: Agent Skills
description: Installable Claude Code skills that set up self-hosted answerLoops and connect an agent to a running workspace.
---
answerLoops ships two [Claude Code skills](https://docs.claude.com/en/docs/claude-code/skills)
directly in this repository, under `skills/`. A skill is a `SKILL.md` file —
Claude Code reads it and follows the instructions inside, so installing one
gives an agent a repeatable, reviewed procedure instead of it improvising the
setup or API calls from scratch each time. The easiest way to install either
one is the `answerloops` CLI, the bin of the
[`@answerloops/agent-sdk`](https://www.npmjs.com/package/@answerloops/agent-sdk)
npm package:
```bash
npx @answerloops/agent-sdk skills answerloops-setup answerloops-operate
```
That writes both into `.claude/skills/`. Install just one by naming it alone.
Both skills are plain files in a public repo — read them before installing if you want to know exactly what they do. Nothing here runs implicitly; a skill only acts when you ask your agent to use it.
## `answerloops-setup`
Installs a self-hosted instance. The mechanical work — prerequisite checks,
cloning, `.env` scaffolding, starting the published image via
`docker compose -f docker-compose.ghcr.yml up -d`, polling `/api/health`
until it's actually up — is done by the same CLI:
```bash
npx @answerloops/agent-sdk setup
```
It stops and hands back to you for anything external — OAuth app creation,
AI provider keys. It never invents a credential: `AUTH_SECRET` and
`ENCRYPTION_KEY` are generated with real randomness in-process, and anything
else required (`DATABASE_URL`, `AUTH_URL`, `AUTH_GOOGLE_ID`,
`AUTH_GOOGLE_SECRET`) it reports as missing rather than guessing at.
Installing the skill (`answerloops-setup`, above) gives an agent the
judgment layer around this — collecting those real values from you in
conversation and re-running the command — rather than you running it solo
and hand-editing `.env` yourself. Either works; the skill just makes it
conversational.
## `answerloops-operate`
Connects an agent to a running workspace — hosted or self-hosted — via the
[MCP server](/docs/integrations/mcp): search the knowledge base, read the
FAQ, list and open tickets, generate grounded answers. Same pipeline every
other channel uses, same per-key scopes and org isolation.
Mint a scoped API key (**Settings → API Keys** in your workspace) and ask
your agent to connect it — the skill walks the rest.
## Which one do I need?
| You want to... | Skill |
|---|---|
| Stand up your own instance | `answerloops-setup` |
| Point an agent at an instance that's already running (yours or hosted) | `answerloops-operate` |
| Both — self-host, then use it | Install both; run setup first |
Both are Claude Code skills today. Support for other agent CLIs/IDEs is
tracked as a future expansion, not yet available.
---
# Doc: integrations/anthropic
URL: https://answerloops.com/docs/integrations/anthropic
---
title: Anthropic
description: Use Anthropic's Claude models for answer generation, with an OpenAI key for embeddings.
---
Anthropic's Claude models generate the answers answerLoops drafts. Claude
handles **chat only** — Anthropic has no embeddings API — so knowledge-base
search still needs an OpenAI key for embeddings alongside it.
Available on every plan. Usage bills to your Anthropic account.
## 1. Get an API key
In the [Anthropic Console](https://console.anthropic.com/settings/keys),
create a key. Make sure the workspace has credit or billing configured.
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** Anthropic.
3. **Model ID:** e.g. `claude-sonnet-4-6`, `claude-haiku-4-5-20251001` — the
field suggests current options; see the
[Claude models list](https://docs.anthropic.com/en/docs/about-claude/models)
for exact names.
4. **API key:** paste your Anthropic key.
5. **Embeddings:** leave the provider on OpenAI and enter an **OpenAI API key**
in the embedding key field. This is required — without it, every
knowledge-base search and every KB import fails after you save. answerLoops
tells you if it's missing.
6. Click **Test connection** — it checks the Claude key and the OpenAI
embedding key separately — then **Save**.
## Models
| Use | Choice |
|---|---|
| Best answers | `claude-sonnet-4-6`, `claude-opus-4-8` |
| Fast / cheap | `claude-haiku-4-5-20251001` |
## Related
- [AI Model settings](/docs/product/ai-config) — the embedding-key requirement
and how provider switching is handled
- [OpenAI integration](/docs/integrations/openai) — for the embedding key
---
# Doc: integrations/circle
URL: https://answerloops.com/docs/integrations/circle
---
title: Circle
description: Connect Circle posts and comments to your support queue and configure reviewed replies.
---
## Overview
answerLoops receives posts and comments from the Circle spaces you select, prepares replies from workspace knowledge, and runs a separate AI review. Configure automatic replies for the questions that qualify, or review drafts with your team.
Inbound events arrive through Circle Workflows configured by your community administrator.
## Setup
### 1. Create an Admin API token
In Circle, go to **Settings → Developers → Tokens**, create a new token, and choose type **Admin V2**. Copy it.
### 2. Connect in answerLoops
Go to **Integrations → Circle** and fill in:
| Field | Value |
| --- | --- |
| Community URL | Your Circle community's base URL, e.g. `https://community.example.com` |
| Admin API token | The token from step 1 (used to fetch the full post/comment body when a webhook payload is thin) |
| Watched space IDs | Numeric space IDs, comma-separated — leave blank to watch every space |
Click **Connect**. The card then shows a **Webhook URL** and a secret token.
### 3. Create the Circle Workflow
In Circle, go to **Settings → Workflows** and create a workflow:
- **Trigger:** *New post*
- **Action:** *Send webhook*
- **URL:** the Webhook URL from step 2, with `?kind=post` appended
- **Header:** `X-AnswerLoops-Token` = the secret from step 2 (use the header whenever the Workflow action supports it — if it can't set headers, append `&token=` to the URL instead)
Create a second workflow the same way for **Trigger:** *New comment*, using `?kind=comment` on its URL instead. Circle's webhook payload shape isn't contractual, so `kind` tells answerLoops which one it's looking at instead of guessing from the payload.
### 4. Set an escalation user (optional)
Enter the Circle user responsible for follow-up in **Escalation user**. Escalation routing is included on Pro and Enterprise.
### 5. Set the confidence threshold
**Confidence threshold** (0–1, default `0.8`) is the score below which the draft is flagged as low-confidence on the ticket.
---
## Environment variables reference
Circle is configured entirely per-organization in the Settings UI. There is no community-wide credential and no environment variable to set. The API token is stored encrypted at rest — set `ENCRYPTION_KEY` before connecting in any real deployment.
---
## Troubleshooting
Check the Circle Workflow is **active** and its recent runs show the webhook action succeeded. Confirm the URL matches exactly, includes the right `?kind=post` or `?kind=comment`, and the `X-AnswerLoops-Token` header (or `token=` query param) carries the secret shown on the Circle card. If you set **Watched space IDs**, a post in any other space is ignored on purpose — clear the field to watch everything. Posts shorter than 10 characters are skipped.
Circle Workflow webhook payloads vary in how much of the post body they include. answerLoops falls back to the Admin API to fetch the full body — make sure the **Admin API token** you saved is a valid **Admin V2** token and hasn't been revoked.
Check the channel's automatic-reply setting and confidence threshold. Review the draft in the ticket queue if it does not qualify for an automatic reply.
---
## Related
- [Discourse integration](/docs/integrations/discourse)
- [Slack integration](/docs/integrations/slack)
---
# Doc: integrations/discord
URL: https://answerloops.com/docs/integrations/discord
---
title: Discord
description: Connect Discord text channels and forum channels to answerLoops for automatic AI deflection and ticket tracking.
---
## Overview
answerLoops integrates with Discord at two levels:
- **Text channels** — every message in a watched channel is ingested as a ticket. The bot replies in-thread when AI confidence is above your threshold **and Automatic Deflections is turned on**.
- **Forum channels** — new forum posts create tickets via the `ThreadCreate` event. The initial post is ingested as the ticket body. Replies inside the forum thread also ingest via `MessageCreate` and are attached to the same ticket.
Both channel types appear in the unified ticket list with a Discord source badge.
**Automatic Deflections** is off by default for every newly connected platform. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss — Discord gets a brief acknowledgment instead of the real answer, or silence. Turn it on per-platform in **Integrations → Discord** once you've reviewed enough approved drafts to trust the AI's answers going out unsupervised.
---
## Cloud setup (recommended)
No Discord Developer Portal access required. answerLoops manages the bot and OAuth app on your behalf.
1. In the onboarding wizard (or **Integrations → Discord**), click **"Add answerLoops to Discord"**
2. Select the server you want to connect in the Discord authorization screen
3. Approve the requested permissions and click **Authorize**
4. Back in answerLoops, a channel picker appears showing all text channels and forum channels on your server
5. Select the channels where the bot should listen and click **Save**
The bot joins your server immediately. No token to copy, no manifest to upload.
You can return to **Integrations → Discord** at any time to add or remove channels without re-authorizing.
---
## Connecting multiple servers
You're not limited to one Discord server. Click **"Add answerLoops to Discord"** again from **Integrations → Discord** to connect another — each server gets its own card with its own channel picker and its own escalation role, and can be removed independently without affecting the others. A server can only be connected to one answerLoops account at a time; if you try to connect a server that's already linked elsewhere, you'll see an error instead of it silently switching ownership.
---
## Self-hosted setup
For self-hosted deployments you need to create your own Discord application and configure three environment variables.
### 1. Create a Discord application
1. Go to [discord.com/developers/applications](https://discord.com/developers/applications) and click **New Application**
2. Under **Bot**, click **Add Bot** and copy the **Bot Token** → this is `DISCORD_TOKEN`
3. Under **OAuth2 → General**, copy the **Client ID** → `DISCORD_CLIENT_ID`
4. Copy the **Client Secret** → `DISCORD_CLIENT_SECRET`
5. Add your callback URL to **OAuth2 → Redirects**: `{AUTH_URL}/api/discord/callback`
### 2. Set required permissions
Under **Bot → Privileged Gateway Intents**, enable:
- **Message Content Intent**
- **Server Members Intent** (required for user lookups)
Under **OAuth2 → URL Generator**, select scopes: `bot`, `applications.commands`. Select bot permissions: **Read Messages/View Channels**, **Send Messages**, **Create Public Threads**, **Read Message History**.
### 3. Configure environment variables
Set these on your `app` service (not only on the bot service — the app service needs `DISCORD_TOKEN` for the channel picker to work):
| Variable | Description |
|---|---|
| `DISCORD_TOKEN` | Bot token from the Discord Developer Portal |
| `DISCORD_CLIENT_ID` | Application (client) ID |
| `DISCORD_CLIENT_SECRET` | OAuth2 client secret |
| `DISCORD_APPLICATION_ID` | Same as `DISCORD_CLIENT_ID` — required for slash commands |
| `BOT_TARGET_URL` | Internal URL the bot uses to reach the app API (no trailing slash) |
### 4. Register the OAuth callback
Make sure `AUTH_URL` is set to your public app URL (e.g. `https://support.yourcompany.com`). Discord will redirect to `{AUTH_URL}/api/discord/callback` after authorization.
---
## Forum channels
Forum channels work without any extra configuration beyond selecting the channel in the channel picker.
When a user creates a new post in a forum channel:
- answerLoops receives a `ThreadCreate` event
- The post's initial message is ingested as a new ticket
- The bot replies in the thread if AI confidence is above threshold
When anyone replies inside that forum thread:
- answerLoops receives `MessageCreate` events for each reply
- Replies are attached to the original ticket for full context
Forum channel replies by answerLoops appear inside the thread, not as top-level posts — keeping conversations organized for your community members.
---
## Slash commands
answerLoops supports two slash commands in connected servers:
| Command | What it does |
|---|---|
| `/ask ` | Queries the KB and returns an AI answer inline (visible only to the user who ran it) |
| `/summarize` | Summarizes the current thread or recent channel activity |
Slash commands require `DISCORD_APPLICATION_ID` to be set. After setting it, register the commands by running:
```bash
pnpm run discord:register-commands
```
---
## Environment variables reference
| Variable | Required | Description |
|---|---|---|
| `DISCORD_TOKEN` | Yes (self-host) | Bot token — set on both `app` and `bot` services |
| `DISCORD_CLIENT_ID` | Yes (self-host) | OAuth2 client ID |
| `DISCORD_CLIENT_SECRET` | Yes (self-host) | OAuth2 client secret |
| `DISCORD_APPLICATION_ID` | For slash commands | Usually the same value as `DISCORD_CLIENT_ID` |
| `BOT_TARGET_URL` | Yes (self-host) | URL the bot uses to call the app API — no trailing slash |
---
## Troubleshooting
`DISCORD_TOKEN` must be set on the **app** service, not only on the bot service. The channel picker fetches the channel list from the Discord API using this token. Restart the app service after adding the variable.
Check that `BOT_TARGET_URL` has no trailing slash (e.g. `http://app:3000`, not `http://app:3000/`). A trailing slash causes the bot's API calls to 404. Also verify **Message Content Intent** is enabled in the Developer Portal.
`DISCORD_TOKEN` and `DISCORD_CLIENT_ID` must belong to the same Discord application. If you regenerated the bot token after setting `DISCORD_CLIENT_ID`, make sure you updated `DISCORD_TOKEN` to the new value.
Confirm the forum channel is selected in **Integrations → Discord → Channel picker**. answerLoops only listens on explicitly selected channels. Also verify the bot has **Read Messages** and **View Channels** permission in that forum.
Run `pnpm run discord:register-commands` and confirm `DISCORD_APPLICATION_ID` is set. Discord can take up to an hour to propagate global slash commands — guild-scoped registration is instant if you specify a `DISCORD_GUILD_ID`.
---
# Doc: integrations/discourse
URL: https://answerloops.com/docs/integrations/discourse
---
title: Discourse
description: Connect a Discourse forum to answerLoops so new topics and replies in watched categories flow through the same confidence-gated support pipeline as every other channel.
---
## Overview
Discourse is the forum platform many open-source projects, product communities, and education communities run as their primary support surface. answerLoops ingests new topics and posts from the categories you choose, runs each one through the standard triage → retrieval → confidence-review → escalation pipeline, and posts grounded answers back into the topic as a bot user.
The connection is per-organization: you create an API key inside your own Discourse admin, scoped to a bot account, and it is stored encrypted in your answerLoops database. There is no shared platform-wide app and no OAuth flow. Inbound events arrive over a Discourse webhook — no polling.
**Automatic Deflections** is off by default, same as every other channel. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss, and the topic gets no automatic reply. Turn it on in **Integrations → Discourse → Edit → Automatic Deflections → Save** once you've reviewed enough approved drafts to trust answers going out unsupervised.
---
## Setup
### 1. Create a bot user
In Discourse, create (or pick) a dedicated account for answerLoops to post as — for example `answerloops-bot`. Give it Trust Level 1 or higher so it can reply in the categories you want covered.
### 2. Create an API key
Go to **Admin → API → New API Key**:
- **User Level:** *Single User*, set to the bot account from step 1 (or *All Users* if you prefer)
- **Scope:** *Granular* is fine — the integration needs `web_hooks` (read/write), `posts` (write), and `categories` (read). *Global* also works.
Copy the key — Discourse only shows it once.
### 3. Connect in answerLoops
Go to **Integrations → Discourse** and fill in:
| Field | Value |
| --- | --- |
| Discourse site URL | Your forum's base URL, e.g. `https://forum.example.com` |
| API key | The key from step 2 |
| Bot username | The account from step 1 |
| Watched category IDs | Numeric category IDs, comma-separated — leave blank to watch every category |
Click **Update** to save.
A category's numeric ID is in its URL: `/c/support/6` is category `6`. It's also shown on the category's settings page.
### 4. Register the webhook
After saving, click **Register webhook** on the Discourse card. answerLoops calls your forum's admin API to create (or update) a webhook that points at `/api/discourse/webhook`, subscribed to topic and post events, signed with a per-organization secret.
If your Discourse instance blocks admin API webhook management, the card also shows the **Payload URL** and **Secret** so you can add the webhook by hand in **Admin → API → Webhooks** — subscribe it to the **Topic Event** and **Post Event** groups, set **Content type** to `application/json`, and leave **Check TLS certificate** on.
Re-register (or update the manual webhook's URL) any time your app's public URL (`AUTH_URL`) changes.
### 5. Set an escalation user (optional)
Enter a Discourse **username without the `@`** in **Escalation user** to have that person mentioned in the topic when AI confidence falls below your threshold.
### 6. Set the confidence threshold
**Confidence threshold** (0–1, default `0.8`) is the score below which an answer is treated as low-confidence — held as a draft, or (with Automatic Deflections on) posted alongside the escalation mention instead of as a full answer.
---
## Environment variables reference
Discourse is configured entirely per-organization through the Settings UI. There is no forum-wide credential and no environment variable to set.
---
## Troubleshooting
Every inbound webhook is verified two ways: the `X-Discourse-Instance` header must match the site URL you saved, and the payload's `X-Discourse-Event-Signature` must be a valid HMAC of the body under the secret answerLoops generated. A 401 means one of those drifted — re-save the integration to mint a fresh secret, then click **Register webhook** again (or update the secret on the manual webhook).
Registration needs the API key to be able to read and write `web_hooks`. If the key is scoped *Granular* without the `web_hooks` scope, or scoped to a non-admin single user, Discourse refuses the call. Recreate the key with the `web_hooks` scope (or a *Global* key) and try again — or add the webhook manually using the URL and secret shown on the card.
Check the webhook is **Active** in **Admin → API → Webhooks** and that its recent deliveries show `200`. If you set **Watched category IDs**, a topic in any other category is ignored on purpose — clear the field to watch everything. Posts shorter than 10 characters and posts by the bot user itself are also skipped.
The API key must be allowed to post as the **Bot username** you configured. A *Single User* key is locked to that one account — make sure it's the same username. The bot account also needs enough trust level to reply in the target category.
answerLoops skips any post whose author matches the configured **Bot username**, so its own replies never re-enter the pipeline. If you see a loop, the bot username in Settings doesn't exactly match the account the API key posts as — fix the username and re-save.
The mention only fires when a post's confidence score is below **Confidence threshold**. If answers are landing above the bar every time, there's nothing to escalate — lower the threshold, or check the ticket's score on the dashboard.
---
## Related
- [Slack integration](/docs/integrations/slack) — the closest existing channel in setup shape
- [Knowledge base](/docs/product/knowledge-gaps) — how the AI decides it doesn't have a confident answer
---
# Doc: integrations/email
URL: https://answerloops.com/docs/integrations/email
---
title: Email
description: Turn a customer-owned support domain into an AI-triaged ticket queue with managed email receiving.
---
## Overview
answerLoops accepts inbound support email through a customer-owned domain configured for managed email receiving, not inbox polling. Every message that arrives is triaged, embedded against your knowledge base, and answered by the AI pipeline, then threaded back to the sender with proper RFC 5322 headers (`Message-ID` / `In-Reply-To` / `References`) so a customer's next reply lands in the same ticket instead of opening a new one.
Two things are independent and configured separately:
- **Inbound mail** — verify a domain you own in answerLoops and add the displayed DNS records. The managed email service sends a signed received-mail event to answerLoops, which retrieves and ingests the message for the matching organization. See [Setup](#setup) below.
- **Outbound sending (the `From:` address)** — either the platform default, a domain you verify, or your own connected Gmail/Outlook mailbox. See [Custom domain](#custom-domain-verified-sending) and [Connect a mailbox](#connect-a-mailbox-gmail-or-outlook-send-only-oauth) below.
All email tickets appear in the unified ticket list with an Email source badge.
**Automatic Deflections** is off by default for every newly connected platform, email included. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss — the customer gets a brief acknowledgment reply instead of the real answer. Turn it on in **Integrations → Email → Edit sender filters & deflections** once you've reviewed enough approved drafts to trust the AI's answers going out unsupervised.
---
## Setup
### 1. Prerequisites
The platform email credentials and webhook signing secret must be set on the app service. They are used for sending, domain verification, retrieving received messages, and verifying signed events.
### 2. Configure in answerLoops
Go to **Integrations → Email** and fill in:
| Setting | Field | What it does |
|---|---|---|
| Allowed senders | `allowedSenders` | Comma-separated list of email addresses and/or domains (e.g. `example.com, partner@other.com`). Only mail from a matching sender is accepted; everything else is silently filtered before it reaches the AI pipeline. Leave blank to accept all inbound mail. |
| Escalation email | `escalationEmail` | Referenced in the reply body when AI confidence comes back below your threshold or a ticket needs human review — email has no @mention concept, so this is surfaced as plain text ("This question has been flagged for human review. `team@yourcompany.com` will follow up."). |
| Confidence threshold | `confidenceThreshold` | 0–1, default `0.8`. Answers scoring below this are routed to a human instead of posting automatically. |
| Automatic Deflections | `autoDeflectEnabled` | See the callout above. Off by default. |
Choose **Use your own domain**, enter the domain or support subdomain that should receive tickets, and submit it. answerLoops registers the domain for both sending and receiving and displays the DKIM, return-path/SPF, and inbound MX records required by the managed email service.
### 3. Add DNS records and verify
Add every record shown in the setup panel at the domain's DNS host. Keep any existing records that your company already uses; if the root domain's MX records belong to Google Workspace or Microsoft 365, use a support subdomain instead so customer mail is not redirected away from the company's mailbox.
Click **Check verification status** after DNS propagation. Once the domain is verified, customers can email:
```
support@yourdomain.com
```
The managed email service delivers signed received-mail events to `POST https://yourapp.com/api/email/ingest`. answerLoops verifies the event, resolves the organization from the recipient domain, retrieves the full message, and sends it through the normal ticket pipeline.
Full self-hosting details, including a deeper reliability writeup (loop detection, idempotency, spam handling) live on the [Email Channel Setup](/docs/self-hosting/email-channel) page.
---
## Custom domain (verified sending)
The professional option for outbound sending — replies go out with your own domain in the `From:` address instead of the platform default, and unlike connecting a personal mailbox, verification doesn't depend on any login staying active.
Without domain verification, a raw `From:` override is spoofing from the receiving mail server's perspective — Gmail/Outlook will flag or reject mail claiming a domain the sending infrastructure was never authorized (via SPF/DKIM) to send as. Verification proves ownership before answerLoops will send as that domain.
1. Go to **Integrations → Email** and find **Use your own domain**.
2. Enter the domain you want replies to come from (e.g. `yourcompany.com`) and click **Use your own domain**.
3. answerLoops registers the domain with the managed email service and shows the required DKIM, return-path/SPF, and inbound MX records — add them at your domain's DNS host.
4. Click **Check verification status** once the records have propagated (this can take a few minutes to a few hours depending on your DNS host).
5. Once verified, customers can send to `support@` and replies automatically send from `noreply@` — no separate toggle or mailbox login is needed.
Removing a verified domain reverts sending to the platform default (`RESEND_FROM`).
---
## Connect a mailbox (Gmail or Outlook, send-only OAuth)
The quick option for outbound sending — connect your own Gmail or Outlook mailbox so replies go out through it directly and inherit its sender reputation, no DNS work required. Unlike the custom-domain path, this depends on the connection staying valid (a password change or long inactivity can revoke it). **Only one mailbox connection can exist per org at a time** — connecting Outlook while Gmail is connected (or vice versa) replaces the existing connection.
answerLoops only ever requests permission to *send* mail as you — it never reads your inbox.
1. Go to **Integrations → Email** and find **Connect a mailbox**.
2. Click **Connect Gmail** or **Connect Outlook** and approve answerLoops on the provider's consent screen.
3. Once approved, replies automatically send from your connected mailbox — no separate toggle needed.
If the consent screen completes but the settings page does not show the mailbox as connected, make sure the OAuth callback URL and the deployment's app URL are configured for the same deployment. Hosted deployments that use a separate app subdomain must also share the authentication cookie across the apex domain so the callback can complete.
If the connection is ever revoked (password change, admin revocation, long inactivity), answerLoops detects it the next time it tries to send, emails your org's admins the same day with a reconnect link, and falls replies back to the platform default in the meantime — nothing is silently dropped. The Email card shows a distinct "connection lost — reconnect" state until you reconnect.
Removing the connection reverts sending to the platform default.
Outlook-sent replies thread slightly differently than platform- or Gmail-sent ones in the customer's own mail client — Microsoft Graph's sending API can't carry a `References` header the way the other paths can, so only partial thread context (`In-Reply-To`) comes through. answerLoops's own internal ticket-threading is unaffected either way.
---
## Environment variables reference
| Variable | Required | Description |
|---|---|---|
| `RESEND_API_KEY` | Yes | Required for every outbound reply |
| `RESEND_FROM` | Yes | Default reply-from address (e.g. `support@yourdomain.com`); used when no custom domain or connected mailbox is set |
| `GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET` | For Gmail connect | A Google Cloud OAuth client requesting only the `gmail.send` scope. Without these, the **Connect Gmail** button is unavailable but every other email path still works |
| `GMAIL_REDIRECT_URI` | No | Overrides the derived callback URL (`/api/email/gmail/callback`) if it differs from `AUTH_URL`/`NEXTAUTH_URL` |
| `OUTLOOK_CLIENT_ID` / `OUTLOOK_CLIENT_SECRET` | For Outlook connect | A Microsoft Entra app registration requesting only the delegated `Mail.Send` scope. Without these, the **Connect Outlook** button is unavailable but every other email path still works |
| `OUTLOOK_REDIRECT_URI` | No | Overrides the derived callback URL (`/api/email/outlook/callback`) if it differs from `AUTH_URL`/`NEXTAUTH_URL` |
The webhook signing secret is used only by the platform endpoint. Customers do not need to know or configure it.
---
## Troubleshooting
Confirm the domain is verified, the inbound MX record matches the record shown in Settings, and the message was sent to `support@yourdomain`. Then check **Allowed senders** — if it's non-empty, mail from a sender not on the list is filtered before the pipeline runs. Also confirm the sender isn't triggering the loop guard (no-reply addresses, mailing-list headers, auto-responder headers).
The platform email credentials must be valid, and the reply-from address (custom domain, connected mailbox, or the configured default) must belong to a verified sending domain (or, for Gmail/Outlook, a currently-connected mailbox). An unverified sending domain prevents delivery; check the app logs for the provider error.
Threading depends on the customer's mail client preserving `In-Reply-To`/`References` headers, which most clients do automatically. If a reply genuinely can't be matched to an existing ticket's Message-ID chain, answerLoops treats it as a new inbound message rather than guessing — this is deliberate to avoid misfiling unrelated mail into an old ticket.
Every inbound email is keyed on its RFC `Message-ID`, so a provider's webhook retry should be a no-op. If duplicates are appearing, check that the sending mail server is generating a stable `Message-ID` per message (some legacy on-prem mail servers omit it, in which case answerLoops falls back to the provider's own id).
Confirm you clicked **Update** after toggling — the setting only takes effect after the form is saved, and the read-only summary view on the card won't reflect an unsaved change. Also note it only changes what happens on **high-confidence** answers; low-confidence answers always go to the draft queue regardless of this setting.
---
# Doc: integrations/github
URL: https://answerloops.com/docs/integrations/github
---
title: GitHub
description: Ingest GitHub Issues and Discussions as tickets, and sync repo markdown and answered Discussions into the Knowledge Base.
---
## Overview
The GitHub integration does three things:
1. **Ticket ingest** — new Issues and Discussions become tickets in answerLoops. AI drafts a reply and posts it back as a GitHub comment automatically when confidence is high enough **and Automatic Deflections is turned on for that repo** (off by default — see [per-repo settings](#5-configure-per-repo-settings)). Issues, comments, and discussions authored by anyone with write access to the repo — owners, org members, and collaborators (GitHub's `author_association` field) — are skipped, so maintainers filing their own work-tracking issues never turn into support tickets or count against your deflection rate.
2. **Markdown KB sync** — markdown files in a repo are embedded into the Knowledge Base. Syncs automatically on every push to the default branch (when enabled) or manually via **Sync now**. Both queue a background job rather than blocking the request.
3. **Discussions KB sync** — answered GitHub Discussions become first-class KB articles: the discussion title is the question, the accepted answer is the answer, attributed to the answering user. A discussion syncs the moment it's marked answered, and un-marking an answer removes the article. **Sync now** also backfills every already-answered discussion in the repo.
## Prerequisites
- A GitHub App created under your account or org
- `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_SLUG`, and `GITHUB_WEBHOOK_SECRET` set in your environment
## 1. Create a GitHub App
Go to **github.com → Settings → Developer settings → GitHub Apps → New GitHub App**.
| Field | Value |
|---|---|
| GitHub App name | Your app name (e.g. `answerLoops`) |
| Homepage URL | `https://yourdomain.com` |
| Callback URL | `https://yourdomain.com/api/auth/callback/github` |
| Setup URL (Post installation) | `https://yourdomain.com/api/github/callback` |
| Webhook URL | `https://yourdomain.com/api/github/webhook` |
| Webhook secret | Any random string — save it as `GITHUB_WEBHOOK_SECRET` |
Under **Permissions**, set:
- **Repository → Issues**: Read & write
- **Repository → Contents**: Read-only (for KB sync)
- **Repository → Metadata**: Read-only
- **Repository → Discussions**: Read-only (or Read & write)
Grant **Repository → Discussions** before trying to subscribe to Discussion events. GitHub only shows event checkboxes for permissions you've already granted — if Discussions isn't set, the **Discussion** and **Discussion comment** checkboxes in the next step won't be visible at all, not greyed out, just missing. If you already saved the app without this permission, add it now, click **Save changes** (its own button, separate from the rest of the page), accept the org re-authorization prompt, then come back to Subscribe to events.
Under **Subscribe to events**, tick:
- Issues
- Issue comments
- Discussions
- Discussion comments
- Push (for automatic KB sync on push)
Save the app. Note the **App ID** and your **App slug** (the URL-safe name).
## 2. Generate a private key
In your GitHub App settings → **Private keys** → **Generate a private key**. This downloads a `.pem` file.
Base64-encode it:
```bash
base64 -i your-app.pem | tr -d '\n'
```
Set the result as `GITHUB_APP_PRIVATE_KEY`.
## 3. Set environment variables
```bash
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_APP_SLUG=
GITHUB_WEBHOOK_SECRET=
```
## 4. Install the app
In your answerLoops dashboard, go to **Integrations → GitHub → Connect GitHub**. This opens the GitHub App install page for your account or org. After authorizing, GitHub redirects back and your repos are auto-discovered.
## 5. Configure per-repo settings
Each connected repo has three controls:
- **Monitored events** — choose Issues, Discussions, Both, or None. This governs *ticket ingest* only.
- **Automatic Deflections** — off by default. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss instead of posting as a GitHub comment automatically — the issue gets a brief acknowledgment comment instead. Turn it on per-repo once you trust the AI's answers going out unsupervised.
- **Knowledge Base source** — toggle KB sync on/off; **Sync now** queues a sync
When KB sync is enabled: every push **to the default branch** queues a re-embed of all `.md` and `.mdx` files (pushes to other branches, tags, or PR refs are ignored), and every discussion marked as answered syncs into the KB immediately (independent of **Monitored events** — a repo can feed the KB from Discussions even if ticket ingest is set to `Issues` or `None`). **Sync now** queues a re-sync of both markdown files and the full set of currently-answered discussions.
Markdown/discussion syncs run in the background rather than inside the webhook or the button click — the KB page shows the job as queued, then syncing, then done, and it's safe to navigate away. Repeated triggers for the same repo while a sync is still running collapse to one job, so a burst of pushes (or a webhook redelivery) never starts overlapping syncs.
## Troubleshooting
Two causes:
1. **Auth guard** — confirm `/api/github/webhook` is in `PUBLIC_PATHS` in `auth.ts`. GitHub sends unauthenticated POSTs; Auth.js blocks them before the route handler runs if the path isn't whitelisted.
2. **Secret mismatch** — `GITHUB_WEBHOOK_SECRET` in your environment must exactly match the webhook secret set in the GitHub App. No leading/trailing whitespace. Regenerate both if unsure.
To diagnose: GitHub App → **Advanced** → **Recent Deliveries** shows the response body. `{"error":"Unauthorized"}` = auth guard. `{"error":"bad signature"}` = secret mismatch.
Check:
- Webhook deliveries show 200 (not 401 or 500)
- Repo's **Monitored events** in Integrations → GitHub is set to `Issues` or `Both`
- The GitHub App has **Issues** ticked under **Subscribe to events**
Almost always a missing app permission, not a config problem in answerLoops. In the GitHub App's **Permissions & events** page:
1. Check **Repository permissions → Discussions** is actually set (Read-only or Read & write) — if it's blank, the **Discussion** / **Discussion comment** checkboxes under Subscribe to events don't render at all, so it's easy to think you already subscribed when the app never had the option to.
2. If you just added the Discussions permission, click **Save changes**, accept the org re-authorization prompt, then scroll back down — the event checkboxes only appear after that save round-trip.
3. Confirm **Discussion** and **Discussion comment** are checked under Subscribe to events and saved.
4. GitHub does not retroactively redeliver past events — any test Discussion created before you fixed the permission/subscription needs to be recreated to actually test the fix.
- Confirm the repo contains `.md` or `.mdx` files outside `node_modules`, `vendor`, or test directories
- Check that an AI provider is configured in Settings → AI Model (embeddings require an API key)
- Check Railway/server logs for embedding errors
- Confirm **Knowledge Base source** is toggled on for the repo — Discussions sync follows this flag, not **Monitored events**
- The GitHub App must have **Repository permissions → Discussions** granted, and **Discussion** ticked under **Subscribe to events** — see the permission-before-events note above if the event checkbox was never visible to check
- GitHub only sends the `answered` action when a maintainer explicitly marks a comment as the answer — discussions with replies but no accepted answer are intentionally excluded
- Run **Sync now** to backfill discussions answered before the integration was connected
---
# Doc: integrations/google-chat
URL: https://answerloops.com/docs/integrations/google-chat
---
title: Google Chat
description: Connect a Google Chat space to answerLoops with a one-time connect code — no OAuth flow required.
---
## Overview
Google Chat connects differently than Slack or Discord. Google Chat's app model has no equivalent of Slack's "Add to Slack" OAuth button for installing across an arbitrary customer's Workspace — there's no redirect-based install answerLoops can drive. Instead, an org generates a one-time **connect code** in answerLoops, a Workspace admin adds the answerLoops app to a Chat space themselves, and posting that code in the space pairs it to the org.
Every message in a paired space is ingested as a ticket, exactly like every other channel:
- AI deflection with a configurable confidence threshold — gated by the **Automatic Deflections** toggle below
- Ticket creation and deduplication
- Thread-aware replies — a reply inside a Google Chat thread appends to its existing ticket instead of creating a new one
- Attachment handling — an attachment-only message with no caption is still ingested; each attachment is folded into a `[Attachment: name] — url` line so nothing gets silently dropped
- Escalation mention when AI confidence is low, addressed to a specific Chat user id you configure
**Automatic Deflections** is off by default for every newly connected platform. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss instead of posting to the space automatically — the space gets a brief acknowledgment instead. Turn it on per-platform in **Integrations → Google Chat → Edit escalation, confidence & deflections**, toggle **Automatic Deflections**, then **Save**, once you trust the AI's answers going out unsupervised.
v1 supports one connected Google Chat space per org. Multi-space support (like Discord's multi-server connections) may follow later if there's demand.
---
## The connect-code pairing flow
This is Google Chat's most distinctive setup step, so it's worth walking through exactly what happens on each side.
### 1. Generate a connect code
In **Settings → Integrations → Google Chat**, click **Generate connect code**. This calls `generateGoogleChatConnectCodeAction`, which:
- Checks the org isn't already connected — if a `google_chat` integration row already exists with `enabled: true` and a paired space id, the action refuses and returns an error telling you to disconnect first. Regenerating a code for an already-paired org has no legitimate use case in v1, and silently overwriting the pairing would be surprising.
- Generates a code shaped like `gc_<24 hex chars>` (`crypto.randomBytes(12).toString('hex')`, prefixed).
- Writes an `integrations` row with that code as `bot_secret` and **`enabled: false`** — the row exists to hold the pending code, but it isn't a live connection yet.
The code is shown once in the Settings UI along with the remaining steps.
### 2. Add the answerLoops app to a Google Chat space
You (or your Workspace admin) add the answerLoops Chat app to the space you want connected — the same app every answerLoops org uses, hosted under answerLoops' own Google Cloud project. If your Workspace admin hasn't enabled third-party Chat apps for the domain yet, they'll need to do that first from their Admin Console before the app can be added.
### 3. Post the code in that space
Post `/connect ` as a message in the space. That message hits the shared inbound webhook (`app/api/google-chat/events/route.ts`), which matches it against `^\/connect\s+(\S+)$` before doing anything else with the event. The handler looks the code up with `getIntegrationByPairingCode` — deliberately **not** filtered on `enabled`, since the whole point of this lookup is to find the still-pending row created in step 1.
- **Code not recognized** (typo, already consumed, or never generated): the space gets back "That connect code wasn't recognized — check Settings → Integrations → Google Chat for a fresh one." Nothing is written to the database.
- **Code recognized**: `completeGoogleChatPairing(orgId, spaceName)` runs, setting `team_id` to the space's `resourceName` (e.g. `spaces/AAAAxxxxxxx`) and flipping `enabled` to `1`. The space receives "✅ This space is now connected. Questions posted here will flow into your answerLoops dashboard."
### 4. Confirm on the Settings page
Back in answerLoops, leave the Google Chat card open. While a connect code is outstanding it polls for the pairing every few seconds and switches to **Connected · space paired** on its own once pairing completes server-side — there's also a **Check now** button to force it. The card then reveals the connected space id, escalation user, confidence threshold, and Automatic Deflections state. The connect code is stored the moment you generate it, so it survives a page reload; you won't be asked to generate a new one (and won't accidentally invalidate the one you already posted).
The connect code and the "add the app to a space" step can happen in either order — generating the code doesn't require the app to already be in the space, and adding the app doesn't require a code yet. Pairing only completes once `/connect ` is posted in the space *after* the app has joined it.
---
## Authentication model
Unlike Slack and Discord, where each connected workspace/server gets its own bot token, Google Chat authenticates every send as a **single service account shared by every connected org** — the Chat app itself, owned by answerLoops' Google Cloud project, scoped to `https://www.googleapis.com/auth/chat.bot`. There's no per-org token to paste or rotate; routing an outbound reply to the right org's space happens entirely by mapping the message's `spaceName` to the paired `integrations` row.
Inbound events are verified the same centralized way: every request Google Chat sends to the webhook carries a signed OIDC ID token in its `Authorization` header, issued by `chat@system.gserviceaccount.com` with an audience matching `GOOGLE_CHAT_ENDPOINT_URL` exactly. `lib/google-chat/verify.ts` checks both the issuer and the audience before any event is processed — a request that doesn't carry a valid token from Google is rejected with a 403 before it's even parsed.
Outbound replies longer than 2,990 characters are split across multiple messages at the nearest newline (`lib/google-chat/send.ts`), the same conservative threshold used for Slack. A reply into an existing thread sets `messageReplyOption=REPLY_MESSAGE_OR_FAIL` and includes the thread's `resourceName` (`spaces/AAAA/threads/BBBB`) — Google Chat's equivalent of Slack's `thread_ts`.
---
## Escalation and thresholds
Configure these from **Integrations → Google Chat → Edit escalation, confidence & deflections**:
| Field | Format | Notes |
|---|---|---|
| Escalation user id | `users/12345678901234567890` | The Chat resource name of the person to notify when AI confidence is below threshold. Google Chat has no role or group-mention equivalent — escalation always targets one specific user. |
| Confidence threshold | `0`–`1`, default `0.8` | Answers scoring at or above this are eligible to auto-post (if Automatic Deflections is on) or auto-approve as drafts otherwise. |
| Automatic Deflections | on/off, default off | See the callout above. |
A reply inside a tracked thread is ingested even if it's very short — anything under 10 characters that isn't a tracked reply and carries no attachment is dropped as noise (e.g. a lone "ok" or emoji reaction in an otherwise unrelated space). This mirrors the noise filtering other channels apply.
---
## What's different from Slack/Discord
- **No bot token per org.** Sending replies authenticates as the single answerLoops-owned service account described above — nothing to paste or rotate per workspace.
- **No OAuth callback.** Pairing is the connect-code flow above, not a redirect-based "Add to Slack"/"Add to Discord"-style install, because Google Chat's unlisted-app model doesn't hand back an install callback with the space id.
- **No role/group mentions.** Escalation targets exactly one Chat user id, not a role or usergroup.
- **One space per org (v1).** Discord supports multiple connected servers per org; Google Chat currently supports one paired space.
---
## Environment variables reference
These are set once by the platform operator — they configure the single shared Chat app, not anything per-org. Self-hosted deployments must set both; answerLoops Cloud manages them centrally.
| Variable | Required | Description |
|---|---|---|
| `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON` | Yes (self-host) | Full JSON key for the service account used to authenticate outbound sends, as a single-line string |
| `GOOGLE_CHAT_ENDPOINT_URL` | Yes (self-host) | Public HTTPS URL of the inbound webhook (`https://{domain}/api/google-chat/events`) — must exactly match the endpoint configured in the Chat app's connection settings, since Google signs each request's token audience to this value |
See [Google Chat App Setup](/docs/self-hosting/google-chat-app) for the full one-time Google Cloud walkthrough (creating the project, enabling the Chat API, creating the service account, and configuring the app's connection settings and visibility).
---
## Troubleshooting
The code is a one-time token stored on a pending `integrations` row (`enabled: false`). This message means no row matches that exact code — check for a typo, confirm you copied the whole `gc_...` string, or generate a fresh code from **Settings → Integrations → Google Chat** if the old one has already been used to complete a pairing.
`generateGoogleChatConnectCodeAction` refuses to issue a new code while an `enabled: true` row with a paired space already exists, to avoid silently orphaning the current connection. Click **Disconnect** on the Settings card first, then generate a new code to pair a different space.
Confirm the message was posted as `/connect ` with nothing else in the message — the handler matches `^\/connect\s+(\S+)$` exactly. Also confirm the answerLoops app has actually joined the space (it needs to be added before it can see the message at all) and that `GOOGLE_CHAT_ENDPOINT_URL` on the server matches the HTTP endpoint URL configured in the Chat app's connection settings — a mismatch fails the inbound token's audience check and the event never reaches the pairing logic.
The escalation user id must be the Chat resource name, formatted `users/` — not an email address or display name. Grab the correct id from the Google Chat API's people/membership lookups, or from a prior message by that user in the space.
Confirm the space shows **Connected** on the Settings page (an unpaired space is simply ignored — not an error). Also check whether the message is very short (under 10 characters) with no attachment and isn't a reply inside an already-tracked thread; those are filtered as noise by design.
This means either `GOOGLE_CHAT_ENDPOINT_URL` isn't set on the server, or the inbound request's signed token failed verification — check that the configured endpoint URL matches character-for-character (including no trailing slash) what's set in the Chat app's connection settings in Google Cloud Console.
---
# Doc: integrations/google-gemini
URL: https://answerloops.com/docs/integrations/google-gemini
---
title: Google Gemini
description: Use Google's Gemini models for answer generation, with an OpenAI key for embeddings.
---
Google's Gemini models generate the answers answerLoops drafts. This
integration covers **chat only** — knowledge-base search still needs an OpenAI
key for embeddings alongside it.
Available on every plan. Usage bills to your Google AI account.
## 1. Get an API key
Create a key in [Google AI Studio](https://aistudio.google.com/apikey). The
free tier is generous for evaluation; add billing for production traffic.
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** Google Gemini.
3. **Model ID:** e.g. `gemini-2.5-flash`, `gemini-2.5-pro`,
`gemini-2.0-flash` — the field suggests current options; see the
[Gemini models list](https://ai.google.dev/gemini-api/docs/models) for exact
names.
4. **API key:** paste your Google AI Studio key.
5. **Embeddings:** leave the provider on OpenAI and enter an **OpenAI API key**
in the embedding key field — required, or knowledge-base search and KB
imports fail after you save.
6. Click **Test connection**, then **Save**.
## Models
| Use | Choice |
|---|---|
| Balanced | `gemini-2.5-flash` |
| Best answers | `gemini-2.5-pro` |
| Cheapest | `gemini-2.0-flash-lite` |
## Related
- [AI Model settings](/docs/product/ai-config)
- [OpenAI integration](/docs/integrations/openai) — for the embedding key
---
# Doc: integrations/groq
URL: https://answerloops.com/docs/integrations/groq
---
title: Groq
description: Use Groq's fast inference for answer generation, with an OpenAI key for embeddings.
---
Groq runs open models (Llama, Gemma, Mixtral) on its own hardware at very low
latency and cost. Good when you want quick, cheap drafts. This integration
covers **chat only** — knowledge-base search still needs an OpenAI key for
embeddings alongside it.
Available on every plan. Usage bills to your Groq account.
## 1. Get an API key
Create a key in the [Groq Console](https://console.groq.com/keys). It starts
with `gsk_`.
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** Groq.
3. **Model ID:** e.g. `llama-3.3-70b-versatile`, `llama-3.1-8b-instant` — the
field suggests current options; see the
[Groq models list](https://console.groq.com/docs/models) for exact names.
4. **API key:** paste your `gsk_` key.
5. **Embeddings:** leave the provider on OpenAI and enter an **OpenAI API key**
in the embedding key field — required, or knowledge-base search and KB
imports fail after you save.
6. Click **Test connection**, then **Save**.
## Models
| Use | Choice |
|---|---|
| Best answers | `llama-3.3-70b-versatile` |
| Fastest / cheapest | `llama-3.1-8b-instant` |
| Alternatives | `gemma2-9b-it`, `mixtral-8x7b-32768` |
## Related
- [AI Model settings](/docs/product/ai-config)
- [OpenAI integration](/docs/integrations/openai) — for the embedding key
---
# Doc: integrations/mcp
URL: https://answerloops.com/docs/integrations/mcp
---
title: MCP Server
description: Give any AI agent (Claude, Cursor, your own bot) direct access to your knowledge base, FAQ, and tickets via the Model Context Protocol.
---
answerLoops ships an [MCP](https://modelcontextprotocol.io) server so any MCP-compatible agent can search your knowledge base, read the latest FAQ, list/create tickets, and generate grounded answers — the same pipeline that powers Discord, Slack, and email.
This is what makes answerLoops agent-first: the tools below aren't a separate integration bolted on top, they call the exact same triage/answer pipeline every other channel uses, including deflection-limit metering and org-scoped data isolation.
Not using an MCP-native client? The [Agent API](/docs/integrations/agent-api) exposes the same operations as plain REST + OpenAPI, for frameworks like LangChain or AutoGen that don't speak MCP's JSON-RPC transport. Same API key works for both.
Using Claude Code? [Install the `answerloops-operate` skill](/docs/integrations/agent-skills) to get a ready-made procedure for connecting to this server and using the tools below, instead of wiring it up by hand.
## Setup
1. Go to **Settings → API Keys** (workspace owners and admins only — a key grants access to the org's tickets and knowledge base plus metered AI spend, so minting and revoking them is an admin action)
2. Click **Create key**, give it a name (e.g. "Cursor" or "Support bot") and optionally pick an expiry (30/90/365 days, or never)
3. Under **Permissions**, check only the [scopes](/docs/integrations/agent-api#scopes) this client needs — all are checked by default; a tool call the key isn't scoped for is rejected with JSON-RPC error `-32003`
4. Copy the plaintext key shown — it's only displayed once and cannot be recovered later
5. Paste the generated config into your MCP client's config file
```json
{
"mcpServers": {
"answerloops": {
"url": "https://your-instance.example.com/api/mcp",
"headers": {
"Authorization": "Bearer al_live_..."
}
}
}
}
```
Revoke a key any time from the same page. It is rejected immediately and removed from the active-key list. answerLoops retains the revoked database record for audit history without presenting it as a usable workspace credential.
## Transport
Streamable HTTP, JSON-RPC 2.0, single endpoint: `POST /api/mcp`.
- `initialize` and `notifications/initialized` don't require auth (standard MCP handshake)
- Every other method requires `Authorization: Bearer ` — the key resolves to an organization, and every tool call is scoped to that org's data only
- Rate limited per organization (shared across all of that org's keys), enforced globally across every running instance so the limit holds regardless of how many instances are deployed behind it. The ceiling is plan-scaled — 50/minute on Standard, 150/minute on Pro, 300/minute on Enterprise and self-hosted — plus a generous per-IP limit (300/minute) that applies before a key is even resolved. The same counters back the [Agent API](/docs/integrations/agent-api), so a key can't get a second allowance by switching surfaces
- Throttled requests return `429` with a `Retry-After` header (seconds) and JSON-RPC error code `-32002`, which is distinct from the generic internal-error code so your client can back off instead of alerting
- If you send an `MCP-Protocol-Version` header, it must be a revision this server implements (`2024-11-05`) — anything else is rejected up front rather than silently ignored
- Request bodies are capped at 64KB — larger requests get a 413 before the body is read
- Each key is limited to the [scopes](/docs/integrations/agent-api#scopes) it was granted. `tools/list` is unfiltered (so an agent can see what a broader key would unlock), but a `tools/call` for a tool the key lacks the scope for returns JSON-RPC error `-32003` before the tool runs. Every tool definition carries its required scope on `_meta.requiredScope`.
## Tools
Each tool requires one scope, shared with the matching REST endpoint — see the [scope table](/docs/integrations/agent-api#scopes).
| Tool | Required scope | Purpose |
|---|---|---|
| `search_kb` | `kb:read` | Semantic search over published KB articles (promoted from resolved tickets) |
| `get_faq` | `faq:read` | Fetch the most recently generated FAQ digest |
| `get_tickets` | `tickets:read` | List tickets, optionally filtered by `status`, `priority`, or `category` |
| `create_ticket` | `tickets:write` | Open a new ticket — runs through the same AI triage pipeline as every other channel |
| `generate_answer` | `answers:write` | Generate a KB-grounded answer with a confidence score, without opening a ticket |
### search_kb
```json
{ "name": "search_kb", "arguments": { "query": "how do I reset my API key", "limit": 5 } }
```
`query` is capped at 2000 characters. Returns up to `limit` (max 20) matches: `{ question, answer, score }[]`.
### get_faq
No arguments. Returns the latest weekly FAQ digest, or a message if none has been generated yet.
### get_tickets
```json
{ "name": "get_tickets", "arguments": { "status": "open", "priority": "high", "limit": 10 } }
```
All filters are optional. Returns up to `limit` (max 20) tickets, most recent first.
### create_ticket
```json
{ "name": "create_ticket", "arguments": { "content": "Users report webhook retries are duplicated", "authorName": "Slack bot", "idempotencyKey": "a1b2c3d4" } }
```
`content` is required (max 4000 characters). The ticket is tagged `source_platform: "mcp"` and runs through the same category/priority classification and auto-draft pipeline as a Discord or Slack message — it may get auto-answered if confidence is high, otherwise it queues for human review in the dashboard. The tool waits for this pipeline to finish before returning, so the ticket is ready to inspect with its draft or review state. There's no chat channel to post a reply back into, so replies are saved on the ticket for the calling agent to read back via `get_tickets`.
`idempotencyKey` is optional — pass a stable identifier (a UUID, a hash of the content) if your client retries on timeout or network error. Retrying with the same key returns the original ticket (`duplicate: true`) instead of opening a second one and re-running AI triage a second time.
### generate_answer
```json
{ "name": "generate_answer", "arguments": { "question": "What's the rate limit on the widget API?" } }
```
`question` is capped at 2000 characters. Returns `{ answer, confidence, answered_fully, high_confidence }`.
Two limits apply before anything is generated:
| Limit | Counts | Ceiling |
|---|---|---|
| Monthly deflections | High-confidence generations only, pooled with auto-deflected tickets | Your plan's deflection allowance |
| Monthly `generate_answer` calls | Every call, high-confidence or not | 5× your plan's deflection allowance |
The second exists because only high-confidence answers are billed as deflections — the same standard a ticket has to clear to auto-deflect on any other channel. Without a separate ceiling, a caller whose questions consistently score low confidence would never move the deflection counter while still paying for an embedding and two model round trips per call. Both limits are unlimited on plans with unlimited deflections. Hitting either returns an error naming which one.
## Security notes
- Keys are shown once at creation; only a SHA-256 hash is stored server-side
- Creating and revoking keys requires the owner or admin role — members can see which keys exist but not change them
- Every tool call is scoped by the org resolved from the API key — there is no way for one org's key to read or write another org's data
- Each key is further limited to the scopes it was granted, checked before the call does any work
- Revoked and expired keys are rejected before any tool runs
- Usage is recorded against the specific key that made the call, so a suspected leak can be traced to one credential instead of forcing a blanket rotation
### Treat tool output as data, not instructions
`get_tickets` and `search_kb` return text that community members wrote — support tickets and the KB articles promoted from them. Anyone who can file a ticket through any channel can put arbitrary text in there, and that text lands in your agent's context when it calls these tools.
Prompt your agent to treat tool results as untrusted content to reason about, never as instructions to follow. answerLoops keeps the blast radius small by design: `create_ticket` is the only tool that writes anything, and no tool can modify the knowledge base, change settings, or touch billing.
---
# Doc: integrations/mistral
URL: https://answerloops.com/docs/integrations/mistral
---
title: Mistral
description: Use Mistral's models for answer generation, with an OpenAI key for embeddings.
---
Mistral's models generate the answers answerLoops drafts. This integration
covers **chat only** — knowledge-base search still needs an OpenAI key for
embeddings alongside it.
Available on every plan. Usage bills to your Mistral account.
## 1. Get an API key
Create a key in [La Plateforme](https://console.mistral.ai/api-keys). Add a
payment method for production use.
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** Mistral.
3. **Model ID:** e.g. `mistral-large-latest`, `mistral-small-latest`,
`codestral-latest` — the field suggests current options; see the
[Mistral models list](https://docs.mistral.ai/getting-started/models/models_overview/)
for exact names.
4. **API key:** paste your Mistral key.
5. **Embeddings:** leave the provider on OpenAI and enter an **OpenAI API key**
in the embedding key field — required, or knowledge-base search and KB
imports fail after you save.
6. Click **Test connection**, then **Save**.
## Models
| Use | Choice |
|---|---|
| Best answers | `mistral-large-latest` |
| Cheaper | `mistral-small-latest` |
| Code-heavy KBs | `codestral-latest` |
## Related
- [AI Model settings](/docs/product/ai-config)
- [OpenAI integration](/docs/integrations/openai) — for the embedding key
---
# Doc: integrations/notion
URL: https://answerloops.com/docs/integrations/notion
---
title: Notion
description: Connect a Notion workspace and sync its pages and databases into the Knowledge Base so the AI can answer from docs your team keeps in Notion.
---
## Overview
Notion is a **Knowledge Base source**, not a support channel — it doesn't bring in tickets, it feeds the content the AI answers from. Connect a workspace, and answerLoops pulls every page and database your Notion connection can see into the KB, alongside URL crawls, file uploads, GitHub repo sync, and resolved tickets.
Two things make Notion different from the other KB sources:
- **You control scope by sharing.** There's no page picker in answerLoops. A Notion connection only sees pages that have been explicitly shared with it, so you decide what syncs by sharing (or unsharing) pages in Notion.
- **Notion imports unpublished.** Its content is searchable from the dashboard immediately, but it is **not** used to answer customer questions or served to the website widget until you click **Publish to widget** on the Knowledge Base page. Every other source publishes on import; Notion is the exception because workspace docs are often internal drafts. Your publish choice is kept across re-syncs.
## Prerequisites
- A Notion **connection** (access token) created in the workspace you want to sync
- `ENCRYPTION_KEY` set in your environment (the token is stored encrypted at rest)
There is no environment variable to configure for Notion itself — it is set up per workspace in the Settings UI.
This is a five-minute setup, not a one-click connect — Notion requires creating a credential and separately sharing content with it. Each step below is one click or field.
## 1. Create a Notion connection
1. In Notion, open **Settings → Developer**, then click **Open developer tools** (top right) — this opens `notion.so`'s developer portal in a new tab.
2. Click **+ New connection**.
3. Enter a name (e.g. `answerLoops`). Leave **Access token** selected as the authentication method — it's the workspace-scoped option; **OAuth** is for multi-workspace public apps and isn't what you want here.
4. Click **Create connection**.
5. You land on the new connection's page. Under **Capabilities → Content capabilities**, uncheck **Update content** and **Insert content** — leave only **Read content** checked. answerLoops only reads from Notion.
6. Under **Capabilities → User capabilities**, select **No user information** — the sync doesn't need Notion user profiles.
7. Under **Integration token**, click the eye icon to reveal the **Access token**, then copy it (starts with `ntn_` or `secret_`).
## 2. Share the pages you want synced
Creating the connection above grants it *no* content access by itself — you now have to point it at specific pages, one at a time:
1. In Notion, open the page (or top-level page of a section) you want synced.
2. Click **•••** in the top-right corner of the page.
3. Click **Connections**, then select the connection you just created.
Sharing a page also shares everything nested under it, so sharing one top-level parent page is usually enough — you don't need to repeat this for every child page, only for each separate top-level section you want included. answerLoops syncs everything the connection can see this way — nothing more, nothing automatic.
## 3. Connect in answerLoops
Go to **Integrations → Notion**, paste the access token from step 1, and click **Connect**. The card shows the connected workspace name once it validates the token.
## 4. Sync
Go to the **Knowledge Base** page. The Notion panel there has a **Sync now** button. Each sync walks every shared page and database, converts the content to text, splits it into chunks, and embeds it — the same pipeline every other KB source uses. Re-syncing **replaces** the previous Notion content (it does not merge), so removing a page from the integration's access and re-syncing removes it from the KB.
Clicking **Sync now** queues a background job — the panel shows it as queued, then `Syncing N/M` as it works through the pages, then done, and it's safe to leave the page. The sync no longer runs inside the button's request, which used to time out on any sizeable workspace. A second **Sync now** while one is still running is a no-op.
Use **Sync now** to refresh imported content after editing your Notion pages.
A re-sync fetches the whole workspace and builds the replacement before it swaps it in, so an interrupted sync — a Notion outage, a revoked token, a network drop — leaves the previously synced content in place and searchable. The swap only happens once the new import has been built successfully.
## 5. Publish to the widget
Right after a sync, the Notion source shows as **Unpublished** in the Sources list — it's searchable on the dashboard but held back from customer-facing answers and the website widget. When you're happy with what imported, click **Publish to widget** on the Notion panel. **Unpublish** reverses it. Re-syncing preserves whichever state you last chose.
## Troubleshooting
The token was truncated on copy, or the connection was deleted. In Notion, go back to **Settings → Developer → Open developer tools**, open your connection, and copy the access token in full (it starts with `ntn_` or `secret_`).
The connection can't see any pages. In Notion, share at least one page or a parent page under **••• → Connections** — being an admin of the workspace is not enough; the connection itself must be added to a page (or a parent of it) before it can see anything below it. Also confirm an AI provider is configured in **Settings → AI Model**, since embedding requires an API key.
Expected until you publish it. Go to the **Knowledge Base** page → Notion panel → **Publish to widget**. Until then the content only shows in dashboard search.
The block-to-text conversion is pragmatic, not pixel-perfect: nested numbered lists, multi-column layouts, synced blocks and equations don't render exactly, and images, files, and embeds are dropped (their captions are kept). Very deep pages are truncated. The chunker only needs readable prose, so this is usually fine — but if a page relies on a table or diagram to make sense, add a text summary to it in Notion.
There's a hard cap of 2000 KB articles per workspace, shared across all sources. A large Notion workspace can consume the remaining budget; the sync reports what it skipped in its result message. Delete unused sources, or narrow what's shared with the Notion connection, and re-sync.
Separately from the article cap, a single sync reads at most 2000 shared pages and 500 shared databases (the two limits are independent — a large set of pages no longer starves databases). If you hit either, the result message says so. Narrow what's shared with the connection so the parts you care about fit.
## Related
- [Knowledge Base](/docs/product/knowledge-base) — how the KB works, all four import methods, and publishing
- [GitHub integration](/docs/integrations/github) — the other repo/docs KB source
---
# Doc: integrations/ollama
URL: https://answerloops.com/docs/integrations/ollama
---
title: Ollama
description: Run answer generation on a local model through Ollama's OpenAI-compatible API.
---
Ollama runs open models on your own machine and exposes an OpenAI-compatible
API. answerLoops talks to it through the **OpenAI-compatible** provider option
— point it at Ollama's base URL, leave the key blank.
The OpenAI-compatible provider (any custom endpoint, Ollama included) is part
of **Custom AI model configuration**, which is on the Enterprise plan.
Self-hosted deployments have it unconditionally.
Chat runs locally. Embeddings can too (Ollama serves embedding models), or you
can keep embeddings on OpenAI's hosted API — that's the only part that would
leave your network.
## 1. Run Ollama
```bash
ollama serve
ollama pull llama3.2
ollama pull nomic-embed-text # only if you want local embeddings
```
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** OpenAI-compatible.
3. **Base URL:** `http://localhost:11434/v1` — or, when answerLoops runs in
Docker on macOS/Windows, `http://host.docker.internal:11434/v1`.
4. **Model ID:** `llama3.2` (or whatever you pulled).
5. **API key:** leave blank — Ollama doesn't authenticate.
6. **Embeddings:** either
- **OpenAI-compatible** with the same base URL and model `nomic-embed-text`, or
- **OpenAI** with a hosted key (embeddings then leave your network).
7. Click **Test connection** — it will fail if the base URL isn't reachable
from the answerLoops server — then **Save**.
Ollama runs on the host, not inside the answerLoops container. Use
`host.docker.internal` (Mac/Windows) or the host's LAN IP (Linux) as the base
URL, not `localhost`.
## Related
- [Self-hosting: AI providers](/docs/self-hosting/ai-providers) — the same
setup from the self-hoster's angle
- [AI Model settings](/docs/product/ai-config)
---
# Doc: integrations/openai
URL: https://answerloops.com/docs/integrations/openai
---
title: OpenAI
description: Use your own OpenAI API key for answer generation and embeddings.
---
OpenAI is the default AI provider. It's the one provider that covers **both**
jobs answerLoops needs a model for — generating answers and creating the
embeddings that power knowledge-base search — with a single key.
A new cloud workspace runs on answerLoops' key as a free trial (5 AI-answered
tickets). Adding your own key here removes that limit and bills usage to your
OpenAI account. A self-hosted deployment uses the `OPENAI_API_KEY` from its
environment until an org overrides it here.
## 1. Get an API key
In the [OpenAI dashboard](https://platform.openai.com/api-keys), create a
secret key. It starts with `sk-`. You'll also need billing set up on the
OpenAI account.
## 2. Configure in answerLoops
1. **Settings → AI Model → Edit** (owner or admin).
2. **Chat provider:** OpenAI.
3. **Model ID:** e.g. `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5` — the field
suggests current options.
4. **API key:** paste your `sk-` key.
5. **Embeddings:** leave the provider on OpenAI. The same key is used — no
second key to enter.
6. Click **Test connection** to confirm the key and model work, then **Save**.
## Models
| Use | Good choices |
|---|---|
| Answer quality | `gpt-5.6-sol`, `gpt-5.6-terra` |
| Cheaper / faster | `gpt-5.6-luna`, `gpt-5.4-mini`, `gpt-5.4-nano` |
| Reasoning | `gpt-5.5-pro` |
| Embeddings | `text-embedding-3-small` (default), `text-embedding-3-large` |
See the [OpenAI models list](https://platform.openai.com/docs/models) for the
full catalogue and pricing.
## Related
- [AI Model settings](/docs/product/ai-config) — trial, provider switching, and
the test-connection check
- [Self-hosting: AI providers](/docs/self-hosting/ai-providers) — the
environment-level platform key
---
# Doc: integrations/slack
URL: https://answerloops.com/docs/integrations/slack
---
title: Slack
description: Connect a Slack workspace to answerLoops via 1-click OAuth, an optional Events API webhook, or polling mode with no admin approval required.
---
## Overview
answerLoops connects to Slack in one of two ways, and the two are independent of each other:
- **OAuth install** — an admin authorizes answerLoops from the workspace's **Add to Slack** screen. answerLoops receives a bot token (`xoxb-…`) and a channel picker loads automatically. Message delivery into answerLoops then happens either via the **Events API webhook** (real-time) or, on self-hosted deployments only, by falling back to polling.
- **Polling mode (manual setup)** — a bot token, team ID, and channel IDs are pasted in by hand, with no OAuth authorization and no public webhook at all. This is the path for someone who can't get a Slack admin to click "Authorize," or who can't expose a public URL for Slack to call.
Every connected channel appears in the unified ticket list with a Slack source badge. Slack is a Standard-plan-and-above feature — `saveSlackIntegrationAction`, `saveSlackChannelsAction`, and the `/api/slack/install` route all gate on the `slack_integration` entitlement and return an error before touching the workspace if the org's plan doesn't include it.
**Automatic Deflections** is off by default for every newly connected platform. While it's off, even a high-confidence AI answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss — Slack gets a brief acknowledgment reaction/reply instead of the real answer. Turn it on per-workspace in **Integrations → Slack → Edit channels → Automatic Deflections toggle → Save channels** once you've reviewed enough approved drafts to trust the AI's answers going out unsupervised.
---
## Option 1: 1-click OAuth (recommended)
1. In **Integrations → Slack** (or the onboarding wizard), click **Add to Slack**
2. answerLoops requests the `channels:history`, `channels:read`, `channels:join`, `chat:write`, `reactions:write`, and `users:read` scopes and redirects to Slack's authorization screen
3. An admin picks the workspace and approves
4. Slack redirects back to `/api/slack/callback`, which exchanges the code for a bot token and stores it (along with the team ID and an internally-generated `bot_secret` used to authenticate the bot's own traffic back to answerLoops)
5. The channel picker opens automatically, listing every public channel in the workspace via `conversations.list`
6. Select the channels to monitor and click **Save channels**
No token to copy, no manifest to upload. The `state` parameter round-tripped through the OAuth redirect expires after 10 minutes — if you leave the authorization screen open too long before approving, you'll be sent back with an `invalid_state` error and need to click **Add to Slack** again.
### Saving channels joins the bot automatically
Slack never adds a bot to a channel just because a scope was granted — the bot has to explicitly call `conversations.join`. Both `saveSlackChannelsAction` (editing channels post-connect) and `saveSlackIntegrationAction` (manual setup) call this for every selected channel as part of the save, so picking a channel and clicking **Save** is enough to make it work immediately — there's no separate "invite the bot" step for public channels.
**Private channels are the one exception.** Slack doesn't let any bot self-join a private channel — this is a platform limit, not something answerLoops works around. If a save includes a private channel, the join call fails with `not_in_channel`-adjacent errors and the UI shows a warning like:
> Joined 2/3 channels automatically. Private channels need a manual invite in Slack (Channel → Integrations → Add apps): C09876ZYXWV
You still need to `/invite @answerLoops` (or add the app via that channel's **Integrations → Add apps**) for any private channel yourself.
---
## Events API webhook (real-time delivery)
The Events API webhook is what gives OAuth-connected workspaces instant delivery. It's automatic on managed cloud (the platform has one shared webhook URL already configured) and requires one extra step for self-hosted deployments:
1. In your Slack app's **Event Subscriptions**, set the Request URL to `{YOUR_DOMAIN}/api/slack/events` — the self-hosted Integrations page surfaces this exact URL once Slack is connected
2. Subscribe to the `message.channels` and `reaction_added` bot events
3. Slack sends a `url_verification` challenge on save; `/api/slack/events` answers it before any signature check runs
4. Every subsequent event is verified against `SLACK_SIGNING_SECRET` using the `x-slack-request-timestamp` and `x-slack-signature` headers — a request that fails verification gets a `403`, and an event for a team answerLoops has no integration record for is rejected the same way
Once wired up, new messages create tickets in real time, and 👍/👎 or 1️⃣–5️⃣ reactions on a bot reply are read as feedback votes or CSAT ratings respectively.
---
## Option 2: Polling mode (no admin, no webhook)
Self-hosted only — every managed-cloud org already gets real-time delivery through the shared Events API webhook, so cloud never starts the polling loop at all. Polling exists for operators who can't get Slack admin approval for an OAuth install, or who can't expose a public webhook URL to Slack for security-review or firewall reasons.
1. Go to **Integrations → Slack** and click **Set up manually instead** (only shown on self-hosted deployments, next to **Add to Slack**)
2. Paste a bot token (`xoxb-…`), the workspace's Team ID (`T…`), and one or more channel IDs
3. A signing secret is optional here — only required if you also plan to wire up the Events API webhook alongside polling
4. answerLoops calls `conversations.join` on every listed channel the same way OAuth does, then starts polling `conversations.history` on that channel every `SLACK_POLL_INTERVAL_SECONDS` (default 60, minimum recommended 30)
**A channel's first poll never backfills history.** The poller seeds its cursor to the newest message already in the channel and tickets nothing on that pass — exactly like Discord's gateway, which never sees anything posted before the bot joined. Only messages posted after the first poll become tickets. This is deliberate: without it, connecting Slack to an active community channel would flood the ticket list with the channel's entire recent history on day one.
**Why some deployments choose polling over OAuth:**
- No inbound HTTP from Slack to your servers — the bot only ever calls out to `slack.com`
- No public webhook URL required, so nothing new to expose past a firewall
- Narrower security review scope: one outbound API call per poll interval, versus an inbound endpoint that has to verify every request's signature
- A read-only-scoped token is enough — no OAuth app, no admin authorization flow
```bash
SLACK_POLL_INTERVAL_SECONDS=60 # default. Minimum recommended: 30.
```
Polling and the Events API webhook aren't mutually exclusive on self-hosted deployments — a workspace connected via manual token entry can still have a signing secret set and receive webhook events if you wire up Event Subscriptions for it. In practice most operators pick one or the other.
---
## Feature comparison
| | OAuth + Events API webhook | OAuth (self-hosted, polling) | Manual polling |
|---|---|---|---|
| Admin authorization needed | Yes | Yes | No |
| Public webhook URL required | Yes | No | No |
| Message latency | Instant | Poll interval (default 60s) | Poll interval (default 60s) |
| Channel picker | Automatic | Automatic | Manual (paste IDs) |
| Signing secret needed | Yes, to verify inbound events | No | Optional |
| Auto-joins selected channels | Yes (public only) | Yes (public only) | Yes (public only) |
| Available on managed cloud | Yes (default) | No | No |
---
## Escalation, confidence, and deflection settings
The channel picker's save form (**Integrations → Slack → Edit channels**) also sets three per-workspace fields stored on the integration record:
- **Escalation User Group ID** (optional) — a Slack user group or user ID (`S…` or `U…`) pinged when the AI's confidence is below threshold
- **Confidence threshold** (0–1, default `0.8`) — the AI answer confidence required before answerLoops treats a question as answerable at all
- **Automatic Deflections** — off by default; see the callout above
These map directly to the `escalation_role_id`, `confidence_threshold`, and `auto_deflect_enabled` fields on the integration record, alongside `bot_token`, `team_id`, and `channel_ids`.
---
## Environment variables reference
| Variable | Required | Description |
|---|---|---|
| `SLACK_CLIENT_ID` | For OAuth install | OAuth app Client ID — enables the **Add to Slack** 1-click flow |
| `SLACK_CLIENT_SECRET` | For OAuth install | OAuth app Client Secret, paired with `SLACK_CLIENT_ID` |
| `SLACK_SIGNING_SECRET` | For Events API webhook | Verifies inbound webhook payloads at `/api/slack/events`; platform-wide, not per-workspace |
| `SLACK_POLL_INTERVAL_SECONDS` | No | Polling interval in seconds. Default `60`, minimum recommended `30` |
| `AUTH_URL` | Yes (self-host) | Public app URL — Slack redirects to `{AUTH_URL}/api/slack/callback` after authorization |
| `BOT_TARGET_URL` | For polling | Internal URL the poller uses to forward ingested messages to the app's `/api/ingest` — no trailing slash |
Add `{AUTH_URL}/api/slack/callback` to your Slack app's **OAuth & Permissions → Redirect URLs** before anyone connects via OAuth.
---
## Troubleshooting
Saving a channel selection calls `conversations.join` for every public channel automatically, so this usually means the channel is **private**. Slack doesn't allow any bot to self-join a private channel — invite it manually with `/invite @answerLoops` or via that channel's **Integrations → Add apps**, then re-save the channel list so the poller or webhook path picks it up.
The `users:read` scope resolves user IDs to display names via `users.info`. It was added after some workspaces first connected, and OAuth grants aren't retroactive — a workspace that authorized before this scope existed keeps its old-scoped token. Disconnect and reconnect Slack (**Integrations → Slack → Disconnect**, then **Add to Slack** again) to pick up the new scope.
This shouldn't happen — a channel's first poll intentionally seeds its cursor to the newest existing message and tickets nothing on that pass, so only messages posted after the connection become tickets. If you're seeing old history come in as tickets, check whether the channel was previously connected and disconnected (the cursor is tied to org + channel and isn't reset on disconnect).
Either the signing secret doesn't match what's configured in your Slack app's **Basic Information → Signing Secret** (make sure `SLACK_SIGNING_SECRET` on the server matches), or answerLoops has no integration record for that team ID at all — confirm the workspace completed OAuth or manual setup with the matching Team ID before wiring up Event Subscriptions.
Slack is a Standard-plan-and-above feature. `/api/slack/install`, `saveSlackIntegrationAction`, and `saveSlackChannelsAction` all check the `slack_integration` entitlement and return an error before contacting Slack if the org's current plan doesn't include it — upgrade the plan and retry.
**Set up manually instead** only appears on self-hosted deployments — managed cloud always has a working Events API webhook, so the polling fallback is intentionally hidden there. If you're self-hosting and don't see it, confirm the deployment mode detection (`getCurrentDeploymentMode`) is correctly reporting `self-hosted`.
Only 👍/👎 (`+1`/`thumbsup`, `-1`/`thumbsdown`) map to feedback votes, and only 1️⃣–5️⃣ (`one` through `five`) map to CSAT ratings — other emoji reactions are ignored. The reaction also has to land on the specific message answerLoops posted (the answer message for votes, the CSAT prompt message for ratings); reacting on the original question does nothing.
---
# Doc: integrations/stripe
URL: https://answerloops.com/docs/integrations/stripe
---
title: Stripe
description: How answerLoops connects billing to Stripe.
---
answerLoops uses Stripe Checkout for hosted-plan trials and subscriptions. Stripe
handles payment details, trial timing, invoices, and the customer portal; the
application stores the subscription state needed to grant access.
## Customer lifecycle
Signing in with Google creates an answerLoops user and workspace, but it does
not create a Stripe Customer. Opening or abandoning Checkout also does not count
as becoming a customer. A Stripe Customer is attached to the workspace when
Stripe sends the successful `checkout.session.completed` webhook, at the same
time the subscription and welcome email are processed.
When an existing subscriber changes plans, answerLoops reuses that workspace's
Stripe Customer so the billing history stays together.
## Webhook setup
For a cloud deployment, configure Stripe to send these events to
`/api/billing/webhook`:
- `checkout.session.completed`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `invoice.payment_failed`
Set `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` in the deployment
environment. The webhook is the source of truth for the local subscription
record, so access can take a short moment to appear after Checkout redirects
back to answerLoops.
---
# Doc: integrations/telegram
URL: https://answerloops.com/docs/integrations/telegram
---
title: Telegram
description: Connect a Telegram bot to answerLoops so questions in your group chats get AI-drafted answers and show up in your ticket list.
---
## Overview
answerLoops connects to Telegram through a bot you create and control — there's no shared platform-wide bot and no OAuth flow. Every message sent to the bot in a monitored chat (10+ characters, from a real user rather than another bot) is ingested as a ticket. The bot replies in the same chat when AI confidence is above your threshold **and Automatic Deflections is turned on**; otherwise the answer is held as a draft on the dashboard and the chat gets no reply.
Because the bot is per-organization, the token you generate with @BotFather is stored encrypted in your answerLoops database — it's never shared with other orgs, and self-hosted deployments don't need a platform-wide credential to make Telegram work. answerLoops never displays a saved token back to you (even the last few characters) — the Bot Token field always shows a masked placeholder confirming one is saved, and typing in it replaces the token rather than editing it.
**Automatic Deflections** is off by default, same as every other channel. While it's off, even a high-confidence answer is held as a draft on the dashboard awaiting Approve/Edit/Dismiss, and the Telegram chat gets no automatic reply. Turn it on in **Integrations → Telegram → Edit → Automatic Deflections → Save** once you've reviewed enough approved drafts to trust the AI's answers going out unsupervised.
---
## Setup
### 1. Create a bot with @BotFather
Telegram bots are created by talking to Telegram's own bot, [@BotFather](https://t.me/BotFather), inside the Telegram app — there's no developer portal or web dashboard.
1. Open a chat with **@BotFather** on Telegram
2. Send `/newbot`
3. Choose a display name, then a username ending in `bot` (e.g. `AcmeSupportBot`)
4. BotFather replies with a token that looks like `123456789:AAHdqTcv...` — copy it
Keep this chat open. You'll come back to BotFather later if you need to change the bot's group privacy settings (see Troubleshooting below).
### 2. Paste the token into answerLoops
1. Go to **Integrations → Telegram**
2. Paste the token into **Bot Token** and click **Connect**
answerLoops validates the token's format and calls Telegram's `getMe` API to confirm it's real before saving it. If it's rejected, double-check you copied the whole string including the part after the colon.
### 3. The webhook registers itself
Telegram doesn't push messages to you automatically — it has to be told where to send them. **Connect** does this for you: once the token validates, answerLoops calls Telegram's `setWebhook` API with your app's public URL (`AUTH_URL`) and a per-org secret, and Telegram starts POSTing new messages to `/api/telegram/webhook`. If that call fails (for example `AUTH_URL` isn't a publicly reachable HTTPS URL), the token is still saved and the card shows a **Register webhook** button to retry once the URL is fixed.
Once registration succeeds, the card confirms it — a green **Webhook registered ``** banner replaces the register prompt, with a **Re-register** button if you need to trigger it again. Saving a new bot token clears this confirmation, since a new token needs its own `setWebhook` call before Telegram will deliver to it.
Re-register the webhook (the **Re-register** button on the Telegram card) any time your public URL (`AUTH_URL`) changes. Rotating the bot token and re-saving registers it again automatically.
### 4. Add the bot to a chat and find its chat ID
Add your bot to the Telegram group or supergroup you want answerLoops to monitor (or just message it directly for a 1:1 chat). By default answerLoops listens to **every** chat the bot is a member of — you only need chat IDs if you want to restrict it to specific chats.
To find a chat ID:
1. Add [@userinfobot](https://t.me/userinfobot) to the group and it will post the chat ID, or forward a message from the group to it
2. Group and supergroup chat IDs are **negative numbers** (e.g. `-1001234567890`) — this is normal, not an error
Paste one or more chat IDs, comma-separated (e.g. `-1001234567890, -1009876543210`), into **Chat IDs to monitor** on the Telegram card. Leave it blank to monitor every chat the bot is in.
### 5. Set an escalation username (optional)
If you want a human tagged when the AI isn't confident enough to auto-reply, enter a Telegram **username without the `@`** in **Escalation username**. It's referenced when a message's confidence score falls below your threshold.
### 6. Set the confidence threshold
**Confidence threshold** (0–1, default `0.8`) is the score below which an AI answer is treated as low-confidence — held as a draft (or, with Automatic Deflections on, sent along with the escalation mention instead of posted as a full answer).
---
## Environment variables reference
Telegram is configured per-organization through the Settings UI — there's no bot-wide credential required to make it work. Only one variable is relevant, and it's optional.
| Variable | Required | Description |
|---|---|---|
| `TELEGRAM_BOT_TOKEN` | No | Fallback bot token used only when an org hasn't saved its own token in Settings. Useful for single-tenant self-hosted deployments that want to skip the UI setup step. |
---
## Troubleshooting
answerLoops checks the token against Telegram's expected format (digits, a colon, then a 35-character alphanumeric string) and also calls Telegram's `getMe` API to confirm the token is live. A rejection usually means the token was truncated when copied from BotFather, or the bot was deleted/regenerated — go back to @BotFather, send `/token` for your bot, and copy the fresh value.
Connecting calls Telegram's `setWebhook` API with your app's public URL. If `AUTH_URL` isn't set to a real, publicly reachable HTTPS URL, Telegram refuses it — `localhost` and private/internal hostnames don't work here. The token is still saved; set `AUTH_URL` to your public app URL and click **Register webhook** on the Telegram card to finish. Until then the bot receives nothing.
Telegram bots run in **privacy mode** by default in group chats: they only receive messages that start with `/`, mention the bot by `@username`, or are replies to the bot's own messages — everything else is invisible to the bot even though it's in the group. If you want answerLoops to see all group messages, message @BotFather, send `/setprivacy`, select your bot, and choose **Disable**. This has no effect on 1:1 chats, which the bot always sees in full.
Two filters run before a message reaches the AI pipeline: messages from other bots are always dropped, and messages under 10 characters are skipped as too short to be a real question. If you've set specific **Chat IDs to monitor**, messages from any other chat are silently ignored too — leave the field blank to monitor every chat the bot is in.
Every Telegram webhook call must include the secret token answerLoops registered with `setWebhook`. A 401 means either the secret has drifted out of sync (re-save the integration to generate a fresh one, then re-register the webhook) or something other than Telegram is calling the endpoint directly.
The escalation username only fires when a message's AI confidence score is below **Confidence threshold**. If the AI is answering confidently every time, there's nothing to escalate — lower the threshold if you want more messages routed to a human, or check the ticket's confidence score on the dashboard to see where it's landing.
---
# Doc: introduction
URL: https://answerloops.com/docs/introduction
---
title: What is answerLoops?
description: Prepare and review answers from your documentation across connected support channels.
---
answerLoops collects support questions from connected channels, drafts answers from workspace knowledge, and runs a separate AI review. Your team controls which channels can send qualifying replies automatically.
It works with developer, art, crypto, course, membership, and general-interest communities. You can also add the [chat widget](/docs/product/chat-widget) to any website or documentation site that supports custom JavaScript.
## How it works
1. **Receive the question.** A message in a connected channel becomes a ticket with its source attached.
2. **Retrieve knowledge.** answerLoops searches relevant sources for material to answer the question. The public widget uses published knowledge.
3. **Draft and review.** One AI step prepares the answer; another checks it against the available evidence.
4. **Send or review manually.** Automatic replies must be enabled and meet the configured confidence threshold. Other drafts remain available to your team.
5. **Maintain knowledge.** Promote useful resolved answers and update articles as your product changes.
A confidence score is an AI assessment, not a guarantee that an answer is correct. Test representative questions before enabling automatic replies.
## Capabilities
| Feature | What it does |
| --- | --- |
| AI deflection | Sends qualifying answers automatically when enabled for a channel. |
| Multi-channel ingest | Collects questions from Discord, Slack, Discourse, Circle, GitHub, Telegram, email, Google Chat, and a website widget. |
| Knowledge base | Imports URLs, files, GitHub content, Notion pages, and promoted resolutions. |
| Knowledge gaps | Identifies questions that may need better documentation; Pro and Enterprise. |
| Human escalation | Routes follow-up notifications to configured recipients; Pro and Enterprise. |
| CSAT scoring | Records customer ratings on supported channels; Pro and Enterprise. |
| Simulation mode | Tests the answer workflow without sending live replies; Pro and Enterprise. |
| Ticket list | Collects requests, drafts, replies, and status in one queue. |
| Multi-provider AI | Uses your supported provider account; custom endpoints require Enterprise or self-hosting. |
| Website widget | Adds support chat and lead capture to your site. |
| Billing | Hosted plans with monthly answer allowances, or a self-hosted edition without subscription fees. |
## Ingest channels
| Channel | How it connects | What it ingests |
| --- | --- | --- |
| Discord | OAuth installation (cloud) or bot token (self-host) | Text channel messages, forum thread posts, and replies |
| Slack | OAuth installation or polling mode | Channel messages |
| Discourse | REST API + webhook | Topics and posts in watched categories |
| Circle | API token | Space posts and comments |
| GitHub | GitHub App webhook | Issues, issue comments, Discussions, discussion comments |
| Telegram | Webhook via bot token | Messages in configured chats |
| Email | Provider-agnostic inbound webhook | Any inbound email forwarded to the webhook URL |
| Website widget | Embeddable `
```
3. Paste it into your site's HTML, before the closing `