# 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 `` tag Add the website or documentation domain to **Settings → Widget → Allowed domains**, as described below. The chat bubble appears at the bottom right of pages where you install the snippet. Visitors select it to open the chat panel. ## Adding it by platform Where you paste the snippet depends on how your site is built. Find your platform below. Settings → Custom Code → add code → paste the snippet → set it to load on **all pages**, placement **Body - end**. Settings → Advanced → Code Injection → paste the snippet into the **Footer** field → Save. Online Store → Themes → **Edit code** → open `theme.liquid` → paste the snippet right before `` → Save. One edit covers every page. Install a header/footer plugin (e.g. "Insert Headers and Footers"), paste the snippet into the **footer** field, and save. Avoids hand-editing `footer.php`, which a theme update can overwrite. Project Settings → Custom Code → paste the snippet into **Footer Code** → Save → Publish. Custom code only takes effect after a publish, not just a save. Load it once from the file that wraps every page, using `next/script` so it doesn't block rendering. **App Router** — `app/layout.tsx`: ```tsx import Script from 'next/script' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` Paste the raw snippet into `src/index.html`, before `` — Angular's static app shell, equivalent to Vite's `index.html`: ```html ``` Whichever platform you use, the rule is the same: paste into the **one file or field that wraps every page** (a layout, an app shell, a footer-code field) — never a single page's content, or the widget only loads there. ## Restricting where the widget works The `data-widget-id` value is an **embed token**, and it is deliberately public: it ships in the HTML of every page the widget renders on. There is no way to hide it, which means anyone can copy it out of your page source and use it on their own site — putting your knowledge base and your AI spend behind it. **Settings → Widget → Allowed domains** is how you stop that. List the domains you embed on, one per line: ``` example.com docs.example.com ``` Subdomains of a listed domain are included, so `example.com` also covers `support.example.com`. Ports are ignored. Embeds on any other domain show a short message explaining that the widget is not enabled there, rather than failing silently. **The widget will not load anywhere until you list at least one domain.** This is deliberate: an allowlist that defaults to open protects only the people who already thought about it. Your own answerLoops domain is always permitted, so the **Preview widget** link in Settings works without any configuration. ### How to think about the allowlist The allowlist controls **where your widget renders**. If someone copies your embed snippet out of your page source and drops it on their own site, that embed is refused. Treat it as a scoping control rather than as authentication. Your embed token is a public identifier — it appears in your page HTML by design, the same as any embedded widget — so the allowlist is what keeps it useful only where you intend, not a secret that proves who is calling. Usage limits apply independently of it. Only workspace owners and admins can change the allowed domains or regenerate the token. Members can see the embed snippet but not alter where it works. ### Documentation sites If your docs run on a platform that executes custom JavaScript on every page — Mintlify, Docusaurus, and most others do — you can mount the widget there without editing templates. Drop a small script that injects the snippet, and add the docs domain to your allowed domains. This is usually the highest-value place to put it: docs visitors are anonymous and arrive with support-shaped questions, which is exactly what the knowledge base answers. On platforms with no build step, this means the embed token ends up committed to whatever repository holds your docs. The token is a public identifier rather than a secret — it appears in page HTML regardless — but committing it does mean rotating it requires a commit. If that matters for your setup, hold off until the hosted loader ships, which replaces the token in the snippet with a stable slug. ## Rotating the token Tokens expire after **90 days**. Settings warns you as the date approaches, and **Regenerate token** issues a new one. Regenerating takes effect immediately and invalidates the old token. Every page still using the old snippet stops working until you update it. Rotate when you are able to update your site, not before. ## Rate limits & input caps The widget chat endpoint is public (no login required), so it enforces limits to protect your AI provider bill from abuse: - Up to **100 requests/minute per widget token** (caps total cost exposure for one site even if traffic comes from many IPs) - Up to **20 requests/minute per visitor** (token + IP combination) - Each message capped at **4,000 characters**; at most **50 messages** per request - Request bodies capped at **512KB** The lead-capture endpoint, used when a visitor leaves an email address, is limited separately at **30/minute per token** and **5/minute per visitor**, with a 64KB body cap and a validated address. Requests over these limits return `429 Too Many Requests`. Limits are enforced across every running instance rather than per process, so the numbers above are the real ceiling. They are not currently configurable. ## What the widget can and cannot reach It answers from your **knowledge base** — published articles only. It does not read your ticket queue, and it has no access to settings, billing, or team data either; the embed token grants none of those. Treat knowledge base contents as visible to anyone who can use the widget. If you have ingested internal documentation, review it before making the widget public. --- # Doc: product/csat URL: https://answerloops.com/docs/product/csat --- title: Customer satisfaction description: Read customer ratings alongside answer feedback. --- Customer satisfaction (CSAT) scoring is available on **Pro and Enterprise**, and in the self-hosted edition. ## How ratings work On channels that support the rating prompt, customers can rate an AI answer from 1 to 5. A rating is associated with the ticket and appears in the workspace's aggregate results. ## Review results Open **Analytics → Customer satisfaction (CSAT)** to see: - **Average rating:** the mean of submitted ratings, shown out of 5. - **Total ratings:** the number of responses received. - **Rating distribution:** the count at each score from 1 to 5. If no ratings have been submitted, the panel shows an empty state. A lack of responses is not a positive or negative satisfaction result. ## Interpret feedback Review the response count alongside the average. A small number of ratings can change the result substantially. Use low ratings to investigate the original answer and its documentation, rather than treating the score as a diagnosis. CSAT and thumbs-up/thumbs-down answer feedback are separate measures. The latter appears in **Answer quality by category**. See [Analytics](/docs/product/analytics) for how each metric is calculated. --- # Doc: product/dashboard URL: https://answerloops.com/docs/product/dashboard --- title: Dashboard description: Operate support, knowledge, and AI performance from one workspace. --- Use the dashboard to review open work, answer outcomes, and response targets. The sidebar links to tickets, knowledge, reporting, and workspace settings. ## Stat cards | Card | What it shows | |---|---| | Auto-Answered | Questions resolved automatically by AI | | Deflection rate | Deflected / Total — your key efficiency metric | | Open | Questions that still need attention | | In Progress | Questions currently being handled | | Resolved | Completed support conversations | | Needs Review | Answers waiting for a human decision | | AI Drafts Pending | Draft replies ready to review | | SLA Breaches | Tickets past their configured response target | Tickets marked `duplicate` are left out of every stat card, the deflection rate, category breakdown, and SLA figures, so a repeated question is only counted once. Mark a ticket as duplicate from its status form on the ticket page. ## Deflection rate A deflection happens when the AI answers a question with confidence above your threshold (default 0.8) and the user doesn't escalate to a human. Higher = less manual support work. Tickets classified as `bug` are excluded from this metric entirely — the knowledge base can't contain a fix that doesn't exist yet, so declining to auto-answer a bug report is correct behaviour, not a miss. Deflection rate reflects how well answerLoops handles questions the knowledge base can actually answer. ## Quick actions The dashboard surfaces quick links to the most common tasks: adding knowledge, reviewing deflection trends, configuring AI behavior, and reviewing the generated FAQ. The open-ticket panel prioritizes unresolved work, while the SLA panel separates time-sensitive conversations from the rest of the queue. ## Live updates Tickets, draft availability, and team membership update while the dashboard is open. If the connection is interrupted, the page reconnects and refreshes. Returning to a background tab also refreshes its data. Self-hosted operators using a pooled database connection should configure `DIRECT_DATABASE_URL` for live updates. See [environment variables](/docs/self-hosting/environment-variables). ## Navigation The workspace sidebar groups pages by the job they support: - **Monitor:** Dashboard, Analytics, and Simulation - **Support:** Tickets and widget Leads - **Improve:** Knowledge Base, FAQ, and Knowledge Gaps - **Configure:** Billing and Settings On mobile, the same navigation is available from the menu button in the top bar. --- # Doc: product/escalation URL: https://answerloops.com/docs/product/escalation --- title: Human escalation description: Configure who should follow up when a question needs a person. --- Drafts that do not qualify for automatic replies remain available for your team. **Human escalation routing**, available on Pro and Enterprise and in the self-hosted edition, adds channel-specific notifications to the people responsible for follow-up. ## Configure a destination 1. Open **Settings** and select the connected channel. 2. Edit its escalation destination. The field depends on the platform: a Discord role, Slack group, Telegram username, email address, or supported channel user. 3. Save the configuration. 4. Test with a question that needs human help. Confirm that the ticket appears and the intended person or group receives the notification. Use the integration's setup guide for platform-specific identifiers and permissions. ## Automatic replies and confidence The confidence threshold determines which reviewed answers qualify for automatic replies when that channel has automatic replies enabled. Raising the threshold makes automatic replies more selective. Lowering it allows more drafts to qualify; it does not improve the evidence behind them. Before changing the threshold, inspect the draft and its sources. Missing or outdated documentation should be corrected in the knowledge base. ## Handle the ticket Open the ticket to read the question and draft. Edit or replace the draft, send the response, and update the ticket status. Promote a resolution into knowledge only when it is useful beyond this individual request. See [Tickets](/docs/product/tickets) and [AI Model](/docs/product/ai-config). --- # Doc: product/faq-generator URL: https://answerloops.com/docs/product/faq-generator --- title: FAQ Generator description: Auto-generate a weekly FAQ from resolved tickets and publish it anywhere with one click. --- ## What is the FAQ Generator? The FAQ Generator turns your resolved support tickets into a structured FAQ document. Each week, answerLoops scans all tickets that were resolved in the past 7 days, clusters them by topic, and uses AI to write clean Q&A pairs — one FAQ entry per question cluster. The result is a living FAQ that reflects what your community is actually asking, not what you assumed they'd ask when you wrote your original docs. --- ## How to generate a FAQ 1. Go to **FAQ** in the left sidebar 2. Click **Generate from this week's tickets** 3. answerLoops calls the AI with all resolved tickets from the past 7 days 4. The FAQ appears on the page — review and copy it wherever you publish docs Only **resolved** tickets count. Open or in-progress tickets are excluded. If you click Generate and see "No tickets to generate from", resolve some tickets first or check that your tickets have been updated to resolved status. --- ## What a FAQ entry looks like Each entry contains: - **Question** — the original question, cleaned up and normalized - **Answer** — the resolution, written as a self-contained answer (not "see the thread above") - **Source ticket link** — links back to the original ticket for full context Multiple tickets that ask the same thing are merged into one FAQ entry. --- ## FAQ history Every time you generate a FAQ, answerLoops saves a snapshot with: - The week it covers (Monday → Sunday) - How many tickets were included - The full FAQ content You can view past snapshots by returning to the FAQ page — previous weeks are listed below the current week's output. --- ## Copying and publishing The FAQ is plain text (Markdown). Copy it and paste it into: - Your docs site - A Notion page - A Discord `#faq` channel - Your GitHub wiki answerLoops doesn't auto-publish — you control where the FAQ goes. --- ## Troubleshooting **Generate button does nothing / spinner disappears with no output** The most common cause is no resolved tickets in the past 7 days. Go to **Tickets**, filter by status, and confirm some are marked Resolved. If they are and it still fails, check your AI provider key in **Settings → AI Model**. **FAQ shows last week's content even after generating again** Refresh the page after generating. The new snapshot is saved immediately but the page may show a cached version. **FAQ is too long / too many entries** Each resolved ticket contributes at most one entry. To reduce FAQ length, resolve fewer tickets per week (close duplicates without resolving them separately) or edit the output after generating. --- # Doc: product/knowledge-base URL: https://answerloops.com/docs/product/knowledge-base --- title: Knowledge Base description: Import, manage, and search your knowledge base so the AI can deflect questions accurately. --- answerLoops searches the knowledge base for material relevant to a question and uses it to draft a reply. Keep articles current and review imported content before using it for customer answers. ## Import methods There are four ways to add content to the knowledge base. ### 1. URL import Import a web page or an entire documentation site. 1. Go to **Knowledge Base → Import** 2. Paste a URL into the import field 3. Choose **Single page** or **Entire site** - Single page imports only that URL - Entire site crawls linked pages up to a maximum of **25 pages** 4. Click **Import** and wait for processing to finish. Larger sites take longer. 5. Imported pages appear in the Sources list when complete Each page becomes a separate source entry. The crawler fetches the rendered content, splits it into chunks, embeds each chunk as a vector, and writes everything to the database. **Re-import deduplication:** if you import the same URL a second time, pages that were already ingested are skipped automatically. Only new pages are processed. This means you can safely re-run a site import after adding new docs without creating duplicates. Self-hosted operators must configure the URL import service. See [environment variables](/docs/self-hosting/environment-variables#url-ingest--firecrawl-optional). ### 2. File upload Drag and drop files directly into the KB. **Supported formats:** PDF, DOCX, MD, TXT, CSV **Maximum file size:** 50 MB per file 1. Go to **Knowledge Base → Import** 2. Drag files onto the upload area, or click to open the file picker 3. Files are processed immediately — each becomes one or more source entries depending on size Large files are split into chunks automatically. Each chunk is embedded and indexed for semantic search. ### 3. GitHub repo sync Connect a GitHub repository and sync its markdown files into the KB. 1. Go to **Knowledge Base → GitHub Sync** 2. Select the repository from the picker (requires GitHub App installed on your org) 3. Click **Sync now** to queue an import of all `.md` and `.mdx` files 4. Optionally enable **Auto-sync on push** — answerLoops receives a webhook from GitHub on every push to the default branch and queues a re-sync automatically The sync respects the repo's file tree. Each markdown file becomes its own source entry. GitHub and Notion syncs run as background jobs — the Knowledge Base page shows each one as queued, then `Syncing N/M` as it works through the files or pages, then done, and you can navigate away while it runs. Re-triggering a sync for the same source while one is still in progress does nothing; the running job finishes on its own. Install the GitHub App for the repositories you want to sync. Self-hosted operators must also configure the app credentials; see [GitHub setup](/docs/integrations/github). ### 4. Notion workspace Connect a Notion workspace and pull its pages and databases into the KB — useful for teams that keep their support content in Notion rather than a published docs site. Unlike the other three sources, this isn't one click: Notion requires creating a credential in Notion itself, then separately sharing pages with it. Full walkthrough with every click: [Notion integration](/docs/integrations/notion). 1. In Notion, create a connection (**Settings → Developer → Open developer tools → + New connection**, **Access token** auth) and copy its token (starts with `ntn_` or `secret_`). 2. Share the pages you want synced: open each top-level page, click **••• → Connections**, and add your connection. Sharing a page also shares its subpages, so sharing one top-level parent page is usually enough rather than adding every page individually. answerLoops syncs everything the connection can see — you control the scope by sharing or unsharing pages in Notion. 3. In answerLoops, go to **Integrations → Notion**, paste the token, and click **Connect**. 4. Go to the **Knowledge Base** page and click **Sync now** in the Notion panel. Each Notion page (and each row of a shared database) is converted to text, split into chunks, and embedded like every other source. Re-syncing replaces the previous Notion content. Notion content imports **unpublished**. It is searchable from the dashboard but is not used to answer customer questions or served to the website widget until you click **Publish to widget** on the Notion panel. Every other source publishes automatically on import; Notion is the exception because workspace docs are often internal drafts. Your publish choice is kept across re-syncs. The token is stored encrypted (set `ENCRYPTION_KEY` before connecting in any real deployment). There is no environment variable to configure — Notion is set up per workspace in the Integrations UI. ## Sources list The **Sources** tab on the Knowledge Base page lists every imported source. Each row shows: | Column | Description | |---|---| | **Name** | File name, page URL, repo, or "Notion workspace" | | **Type** | `url`, `file`, `github`, or `notion` | | **Chunks** | Number of vector chunks indexed from this source | | **Size** | Approximate content size | A source marked **Unpublished** (currently only Notion) is indexed and shows in dashboard search but is held back from customer-facing answers and the website widget until you publish it from the Notion panel. To remove a source, select it and click **Delete selected**. Deleting a source removes all its chunks from the vector index — any articles derived from it will no longer surface in search results. ## Promoting tickets to the KB When a ticket is resolved with a good answer, you can turn it into a KB article so the AI can reuse that answer for similar future questions. 1. Open a ticket with status `resolved` 2. Click **Promote to KB** in the ticket actions panel 3. The Q&A pair is saved as a new KB article Promoted articles appear in the Sources list with type `ticket`. ## Search Use the search bar at the top of the Knowledge Base page to find articles. **Semantic search** (default when an AI provider is configured): searches by meaning rather than exact words. Asking "how do I reset my password" will surface an article titled "Account recovery steps" even though the words don't match exactly. **Keyword fallback** (when no AI key is configured): falls back to exact text matching. An amber warning banner appears at the top of the KB page to indicate that semantic search is unavailable. To clear a search and return to the full article list, click the **×** inside the search field or the **Clear** button. ## Data persistence KB articles and source chunks are stored in the `kb_articles` and `kb_sources` tables in PostgreSQL. They persist across Docker restarts as long as you retain the `postgres-data` volume. Never run `docker compose down -v`. The `-v` flag deletes all Docker volumes, which permanently removes your KB articles, tickets, and all other data. Use `docker compose down` (without `-v`) to stop the stack safely. --- # Doc: product/knowledge-gaps URL: https://answerloops.com/docs/product/knowledge-gaps --- title: Knowledge Gaps description: Review questions that need better source material or human follow-up. --- ## What are knowledge gaps? A knowledge gap is a question that answerLoops received but couldn't answer well. Gaps appear when: - The AI's confidence score was too low to post an answer - The AI flagged the ticket as needing a human - A ticket was resolved by a human but no matching KB article exists — meaning the next person with the same question will get the same poor result The **Knowledge Gaps** page surfaces all three categories in one place so you can prioritize which content to add next. --- ## Gap reasons Each gap is tagged with one of three reasons: | Reason | When it appears | Condition | |---|---|---| | **Low AI confidence** | The AI searched the KB but wasn't confident enough in any result to post an answer | Confidence score `< 0.6` | | **Needs human** | The AI determined the question requires a human to answer (e.g. account-specific, billing, or policy questions) | `ai_draft_status = needs_human` | | **Missing KB article** | A how-to or docs ticket was resolved by a human but there's no KB article that covers the topic | Resolved ticket with no linked KB content | --- ## The Knowledge Gaps page The page opens with four stat cards at the top: | Card | What it shows | |---|---| | **Total gaps** | All open knowledge gaps across all channels | | **Needs human** | Gaps where the AI flagged the question as requiring human handling | | **Low confidence** | Gaps where the AI searched but confidence was below 0.6 | | **Missing KB article** | Resolved tickets with no covering KB content | Below the stat cards is the gap list. Each row shows: - The original question (truncated) - The **reason badge** (Low confidence / Needs human / Missing KB article) - The **confidence %** for low-confidence gaps - The channel source (Discord, GitHub, Slack, etc.) - A **+ KB** button to jump directly to the Knowledge Base with the ticket pre-selected On the right side of the page, the **Category breakdown** sidebar groups gaps by topic category so you can spot clusters — if 12 gaps are all about your API authentication flow, that's your highest-value content to write next. --- ## How to close a gap Open **Knowledge Gaps** and sort by reason or category. Start with the highest-volume clusters. Click the gap summary to open the full ticket detail. Read the question and the human answer that resolved it (if available). Click **+ KB** next to the gap. This opens the Knowledge Base with the ticket pre-selected. You can: - **Promote the ticket** — turn the human's resolution into a KB article in one click - **Upload a file** — add a doc that covers the topic - **Crawl a URL** — point answerLoops at an existing help article After the import completes, ask a representative question and inspect the draft against the new article. --- ## Review the result After adding an article, test a representative question and inspect the new draft. Check that the answer uses the intended source and addresses the original problem. Adding content does not guarantee that every related question will qualify for an automatic reply. --- # Doc: product/simulation URL: https://answerloops.com/docs/product/simulation --- title: Simulation Mode description: Dry-run the AI pipeline against past tickets without writing to the database. --- Simulation mode lets you test how your AI configuration would have performed on real historical tickets — with zero writes to the database. ## What it does - Fetches the last N tickets (1–100) - Runs each through the full AI pipeline: embed → KB search → generate answer → assess confidence - Compares simulated outcomes to what actually happened - Returns per-ticket results and aggregate stats No tickets are created, updated, or deleted during a simulation run. ## How to run 1. Go to **Simulation** in the sidebar 2. Choose your model, confidence threshold, and ticket count 3. Click **Run simulation** 4. Review results in the table and aggregate stats ## Reading results | Column | Description | |---|---| | Ticket | Question text | | Sim confidence | AI confidence score in this run | | Sim deflect | Would this have been deflected at your threshold? | | Actual deflect | Was it actually deflected in production? | | Match | Do sim and actual agree? | | Duration | Time the AI took to process this ticket | ## Aggregate stats - **Deflect rate** — % of tickets the AI would deflect in this simulation - **Actual deflect rate** — % that were actually deflected in production - **Match rate** — how closely simulation matches production outcomes - **Avg confidence** — mean confidence across all tickets - **Delta** — difference between sim deflect rate and production rate ## Use cases - Test a new model before switching in production - See the impact of raising or lowering the confidence threshold - Diagnose why certain questions aren't being deflected --- # Doc: product/team URL: https://answerloops.com/docs/product/team --- title: Team description: Invite teammates and manage workspace membership. --- Owners and administrators manage invitations in **Settings → Team**. ## Invite a teammate 1. Open **Settings → Team**. 2. Under **Invite a teammate**, enter the person's email address. 3. Select **Member** or **Admin**. 4. Click **Send invite**. You can also copy the invitation link from **Pending Invites**. 5. Ask the recipient to open the link and sign in with the invited email address. The person appears in **Members** after accepting. Pending invitations display their role and expiration date. Use **Revoke** to cancel an invitation that should no longer grant access. ## Roles | Role | Purpose | | --- | --- | | Owner | Holds workspace ownership and can transfer it to another member. | | Admin | Manages workspace settings and membership. | | Member | Works with support tickets and workspace content. | Give administrative access to people who need to manage the workspace. Invitations cannot create another owner; use the ownership-transfer action instead. ## Remove a member Find the person in **Members** and choose **Remove**. The owner cannot be removed with this action. Transfer ownership first when the current owner is leaving. ## Transfer ownership The current owner can select **Transfer Ownership** beside another member and complete the confirmation. Check the selected recipient before confirming: ownership controls administration of the workspace. ## If an invitation does not work Check that the invitation is still listed, has not expired, and matches the email used to sign in. Revoke an incorrect invitation and send a new one with the intended address and role. --- # Doc: product/tickets URL: https://answerloops.com/docs/product/tickets --- title: Tickets description: View, triage, and reply to support tickets from every connected community platform. --- Every question posted in a monitored channel — Discord forum, GitHub Issue, Slack message, Telegram message, or inbound email — becomes a ticket. Tickets track the full lifecycle from first contact through resolution, with AI-generated draft replies posted back to the source platform. ## Ticket list The ticket list is your central inbox. Each row shows: | Column | Description | |---|---| | **#** | Auto-incrementing ticket ID | | **Summary** | AI-generated one-line summary of the question | | **Source** | Colour-coded platform badge (see below) | | **Category** | Auto-classified topic | | **Priority** | `low`, `medium`, `high`, or `urgent` | | **Status** | Current lifecycle state | | **AI** | Whether an AI draft was generated | | **SLA** | Time remaining before SLA breach | | **Author** | Display name of the person who asked | | **Created** | Timestamp of the original message | ### Source platform badges The Source badge identifies where the ticket originated. Colours are consistent across the app: | Platform | Badge colour | |---|---| | Discord | Indigo | | GitHub | Gray | | Slack | Green | | Telegram | Sky | | Email | Yellow | ## Ticket statuses | Status | Meaning | |---|---| | `open` | New ticket, not yet handled | | `in_progress` | Being worked — AI draft generated or human assigned | | `resolved` | Answer provided; eligible for KB promotion | | `closed` | Ticket archived, no further action needed | | `duplicate` | Same question as another ticket. Left out of dashboard counts, analytics, and SLA tracking so it doesn't inflate your numbers | ## Filters Use the filter dropdowns at the top of the Tickets page to narrow by: - **Status** — `open`, `in_progress`, `resolved`, `closed`, `duplicate` - **Priority** — `low`, `medium`, `high`, `urgent` - **Category** — see [Ticket categories](#ticket-categories) below - **Source** — filter to a single platform (Discord, GitHub, etc.) Filters stack — you can combine status + priority + source at the same time. The count in the page header updates live as filters change. ## Ticket categories answerLoops automatically classifies each ticket into one of the following categories: | Category | When it's used | |---|---| | `how_to` | Step-by-step usage questions | | `documentation` | Requests to clarify or find docs | | `bug` | Reports of broken behaviour | | `feature_request` | Suggestions for new functionality | | `general_question` | Everything else that's a genuine question | | `uncategorized` | Could not be confidently classified | ### How category changes the AI response Category isn't just a label — it changes what the AI does next: - **`how_to`, `documentation`, `general_question`** — answered from the knowledge base as usual. The draft is graded for confidence and auto-posted when confidence is high enough; otherwise it's routed to a human for review. - **`bug`, `feature_request`** — never auto-answered from the knowledge base, because the fix or feature doesn't exist there yet. Instead the AI: 1. Checks whether the report matches an existing open ticket and, if so, replies referencing that ticket number. 2. Otherwise checks the knowledge base for a relevant workaround and, if found, posts it explicitly labeled as a workaround, not a fix. 3. Otherwise acknowledges the report and routes it straight to a human — no generated answer is posted. Bug and feature-request tickets never go through the confidence grader and never auto-deflect on their own; a duplicate-match or workaround reply is the only way this category resolves without a human. ## Ticket detail Click any ticket row to open the detail view. ### Platform-aware source link The detail page shows a direct link back to the original message on its platform: - GitHub ticket → **View on GitHub ↗** (opens the Issue or Discussion) - Discord ticket → **View in Discord ↗** (opens the channel/thread) - Slack ticket → **View in Slack ↗** - Telegram ticket → **View in Telegram ↗** - Email ticket → shows the original sender and subject ### AI draft panel When an AI provider is configured, answerLoops generates a draft reply automatically. The AI draft panel appears on the right side of the ticket detail. From there you can: - **Approve** — posts the draft reply directly to the source platform (as a GitHub comment, Discord message, Slack reply, etc.) - **Edit** — opens the draft in an editable text field so you can adjust it before posting - **Dismiss** — hides the draft without posting; the ticket remains open If the AI draft panel never appears, no AI provider is configured. Go to **Settings → AI Model** and add an API key, or configure a per-org model. ### Reply form Below the thread, the reply form lets you send a manual message back to the user. The submit button label reflects the destination platform: - **Send reply to GitHub** - **Send reply to Discord** - **Send reply to Slack** - **Send reply to Telegram** - **Send reply to Email** The reply is posted to the original thread on the source platform, not just stored internally. ### Ticket actions From the detail page you can also: - **Promote to KB** — available on resolved tickets; turns the Q&A into a knowledge base article for future deflection - **Change status** — move the ticket to any status manually - **Change priority** — override the AI-assigned priority ## Export Click **Export CSV** on the ticket list page to download all currently visible tickets (respecting active filters) as a comma-separated file. The export includes all columns shown in the list. --- # Doc: quickstart-cloud URL: https://answerloops.com/docs/quickstart-cloud --- title: Cloud quickstart description: Create a workspace, add knowledge, and test your first channel. --- ## 1. Start the hosted trial Open [answerLoops pricing](https://answerloops.com/pricing), choose a plan and billing interval, and sign in with **Google**. Complete checkout to start the **14-day plan trial**. A card is required; cancel before the trial ends to avoid the first subscription charge. New workspaces also receive a separate, one-time allowance of **five AI-processed tickets** without a model-provider key. Connect your own provider in **Settings → AI Model** to continue after that allowance. Provider usage is billed separately. ## 2. Connect a channel Use onboarding or open the channel in **Settings**. Follow the guide for the platform you want to connect: - [Discord](/docs/integrations/discord) and [Slack](/docs/integrations/slack) - [Discourse](/docs/integrations/discourse) and [Circle](/docs/integrations/circle) - [GitHub](/docs/integrations/github) - [Telegram](/docs/integrations/telegram), [Email](/docs/integrations/email), and [Google Chat](/docs/integrations/google-chat) - [Website widget](/docs/product/chat-widget) You need permission to install or configure the integration in that platform. Select the channels, spaces, or repositories answerLoops should monitor. ## 3. Add knowledge Open **Knowledge Base** and import a current help article, upload documentation, or connect GitHub or Notion content. Wait for the import to complete, then search for a topic from the source to check that it is available. Notion content is initially unpublished. Review it and publish the intended material before using it for customer answers. See [Knowledge Base](/docs/product/knowledge-base). ## 4. Inspect a draft Ask a question covered by the imported content in your connected channel. Open the ticket and check the draft, including whether it answers the question and matches the source. Automatic replies are off by default. A question appearing in the queue without a posted reply can be expected behavior. If no draft appears, check the model configuration and remaining trial allowance. ## 5. Choose reply settings Enable automatic replies for the channels you are ready to automate. A draft must pass the configured confidence threshold before it qualifies. If the source material is missing or inaccurate, correct it before changing the threshold. Invite teammates through [Settings → Team](/docs/product/team), and review [AI Model](/docs/product/ai-config) and [Billing Plans](/docs/reference/billing-plans) before launch. --- # Doc: quickstart-self-host URL: https://answerloops.com/docs/quickstart-self-host --- title: Self-Host Quickstart description: Run answerLoops on your own infrastructure with Docker. --- Use this guide to operate answerLoops on infrastructure you manage. You will configure the application, database, authentication, model services, and channel connections. External model providers and connected platforms still process relevant content; review those services separately from the application’s hosting location. If you'd rather skip all of this, the [cloud quickstart](/docs/quickstart-cloud) gets you the same product with none of the infrastructure. Using Claude Code? [Install the `answerloops-setup` skill](/docs/integrations/agent-skills) and have your agent run this guide for you — it checks prerequisites, scaffolds `.env`, and confirms the instance is actually healthy before handing back. No agent? Run `npx @answerloops/agent-sdk setup` directly — same checks, same steps, plain Node CLI, no Claude Code required. ## Prerequisites - [Node.js 22+](https://nodejs.org) - [Docker + Docker Compose](https://docs.docker.com/get-docker/) - [pnpm 9+](https://pnpm.io/installation) - An AI provider key (OpenAI recommended, or any [supported provider](/docs/self-hosting/ai-providers)) - At least one OAuth provider (GitHub, Discord, or Google) ## 1. Clone the repo ```bash git clone https://github.com/answerLoops/answerLoops.git cd answerLoops ``` ## 2. Configure environment ```bash cp .env.local.example .env.local ``` Edit `.env.local` with at minimum: ```dotenv # Required DATABASE_URL=postgresql://community:community@postgres:5432/community AUTH_URL=http://localhost:3000 AUTH_SECRET= ENCRYPTION_KEY= # At least one OAuth provider AUTH_GITHUB_ID= AUTH_GITHUB_SECRET= # AI (required for deflection and KB search) OPENAI_API_KEY= ``` This project uses **Auth.js v5**. The env vars are `AUTH_URL` and `AUTH_SECRET` — not `NEXTAUTH_URL` / `NEXTAUTH_SECRET`. See the full [environment variables reference](/docs/reference/environment-variables) for all options. ## 3. Start with Docker Once your `.env.local` has real values (not the ones above), this one command does the rest: ```bash docker compose up --build ``` This starts: - **app** on `http://localhost:3000` - **bot** (Discord/Slack listener) connecting to the app - **postgres** on `localhost:5432` (data persisted in a named volume) Drizzle migrations run automatically on first start — no manual migration step. Never run `docker compose down -v` — this deletes your database volume and all data. Use `docker compose down` (no `-v`) to stop without data loss. ## 4. Create your account Open [http://localhost:3000](http://localhost:3000) and sign in with GitHub, Discord, or Google OAuth. On first login you'll see the onboarding wizard: 1. **Connect your community** — Discord 1-click invite, Slack token, or skip 2. **Seed your knowledge base** — drag-drop docs files or paste a URL 3. **Configure AI** — verify your API key and pick a model 4. **Go live** — checklist summary with links to Tickets and KB ## 5. Verify it works ```bash # Health check curl http://localhost:3000/api/health # → {"ok":true} # Confirm tables were created docker compose exec postgres psql -U community -d community -c "\dt" ``` ## Next steps Set up the Discord integration Set up Slack polling or OAuth Connect a Google Chat space Ingest Issues and Discussions Railway, Fly.io, or any Docker host --- # Doc: reference/api/createTicket URL: https://answerloops.com/docs/reference/api/createTicket --- title: Open a new support ticket on behalf of a user description: Runs through the same AI triage/answer pipeline as every other channel (Discord, Slack, email) — the ticket may get auto-answered if confidence is high, otherwise it queues for human review. full: true _openapi: preload: - content/docs/reference/api/openapi.json method: POST webhook: false toc: [] structuredData: headings: [] contents: - content: Runs through the same AI triage/answer pipeline as every other channel (Discord, Slack, email) — the ticket may get auto-answered if confidence is high, otherwise it queues for human review. --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { const { APIPage, OpenAPIPage } = props.components ?? {}; // "APIPage" is the old name from v10, this allows both for backward compatibility const Comp = OpenAPIPage ?? APIPage; return ( <> {props.children} ); } --- # Doc: reference/api/generateAnswer URL: https://answerloops.com/docs/reference/api/generateAnswer --- title: Generate a grounded answer using the organization's knowledge base, without opening a ticket description: Counts against the org's monthly deflection limit — if the limit is reached, returns 429 instead of generating for free. Only high-confidence generations count toward that limit. full: true _openapi: preload: - content/docs/reference/api/openapi.json method: POST webhook: false toc: [] structuredData: headings: [] contents: - content: Counts against the org's monthly deflection limit — if the limit is reached, returns 429 instead of generating for free. Only high-confidence generations count toward that limit. --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { const { APIPage, OpenAPIPage } = props.components ?? {}; // "APIPage" is the old name from v10, this allows both for backward compatibility const Comp = OpenAPIPage ?? APIPage; return ( <> {props.children} ); } --- # Doc: reference/api/getFaq URL: https://answerloops.com/docs/reference/api/getFaq --- title: Get the organization's most recently generated FAQ digest full: true _openapi: preload: - content/docs/reference/api/openapi.json method: GET webhook: false toc: [] structuredData: headings: [] contents: [] --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { const { APIPage, OpenAPIPage } = props.components ?? {}; // "APIPage" is the old name from v10, this allows both for backward compatibility const Comp = OpenAPIPage ?? APIPage; return ( <> {props.children} ); } --- # Doc: reference/api/getTickets URL: https://answerloops.com/docs/reference/api/getTickets --- title: List support tickets for the organization full: true _openapi: preload: - content/docs/reference/api/openapi.json method: GET webhook: false toc: [] structuredData: headings: [] contents: [] --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { const { APIPage, OpenAPIPage } = props.components ?? {}; // "APIPage" is the old name from v10, this allows both for backward compatibility const Comp = OpenAPIPage ?? APIPage; return ( <> {props.children} ); } --- # Doc: reference/api/overview URL: https://answerloops.com/docs/reference/api/overview --- title: Agent API Reference description: Per-endpoint parameter reference for the Agent API, generated from the live OpenAPI spec. --- For setup, auth, curl examples, error shapes, and rate limits, see the [Agent API guide](/docs/integrations/agent-api) — this page is just an index into the detailed parameter reference for each endpoint below, generated directly from the live OpenAPI spec at [`/api/v1/agent/openapi.json`](/api/v1/agent/openapi.json). Semantically search the organization's knowledge base Read the auto-generated FAQ for the organization List support tickets, optionally filtered by status File a new support ticket Run the confidence-gated answer pipeline against a question --- # Doc: reference/api/searchKb URL: https://answerloops.com/docs/reference/api/searchKb --- title: Semantically search the organization's knowledge base full: true _openapi: preload: - content/docs/reference/api/openapi.json method: GET webhook: false toc: [] structuredData: headings: [] contents: [] --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { const { APIPage, OpenAPIPage } = props.components ?? {}; // "APIPage" is the old name from v10, this allows both for backward compatibility const Comp = OpenAPIPage ?? APIPage; return ( <> {props.children} ); } --- # Doc: reference/billing-plans URL: https://answerloops.com/docs/reference/billing-plans --- title: Billing Plans description: Plan tiers, deflection limits, and pricing for answerLoops Cloud. --- ## Plans Pricing is deflection-based, not per-seat. Every hosted plan includes the full AI pipeline — higher tiers add scale, insight, and support. | Plan | Price (monthly) | Price (annual, per mo) | Deflections/mo | Beyond the limit | |---|---|---|---|---| | Standard | $49 | $39 (20%+ off) | 500 | Hard cap — auto-answering pauses | | Pro | $149 | $119 (20%+ off) | 3,000 | Soft cap — keeps answering, $5 per 100 over | | Enterprise | $499 | $399 (20%+ off) | Unlimited | — | There is no free tier. Every plan starts with a 14-day trial — see below. Annual billing saves at least 20%. The annual charges are **$468 Standard**, **$1,428 Pro**, and **$4,788 Enterprise**, paid once per year. The equivalent monthly amounts in the table are not monthly installments. ## What counts as a deflection An automated channel answer counts when it meets the configured confidence threshold without human intervention. High-confidence standalone answers generated through MCP or the Agent API also count. Drafts sent to a human for review do not count. Allowances reset each **calendar month**, including on annual subscriptions. ## Trial Pick a plan at signup and it's fully unlocked for 14 days (card required) — every feature and the deflection limit of the tier you chose, not a reduced trial-only allowance. Cancel any time before the trial ends and you won't be charged. If the trial ends without a valid payment method, access stops immediately — there's no free tier to drop back to. ## Free AI trial Separate from the 14-day plan trial above: every new org also gets **5 lifetime free tickets** fully AI-processed on answerLoops' own key, with no AI provider key required at all. This is a one-time allowance (not monthly, not tied to plan tier) meant to let a brand-new org see the full pipeline work before adding their own key. Once used up, AI processing pauses until a key is added in **Settings → AI Model** — a dashboard banner makes this clear rather than tickets silently going untriaged. Doesn't apply to self-hosted deployments. ## What's included per tier See the comparison table on the [pricing page](https://answerloops.com/pricing) for the full feature breakdown. Summary: - **Standard** — core channels (Discord, Slack, Discourse, Circle, GitHub, Telegram, Email, Google Chat), website widget, knowledge base, bring-your-own AI provider, CSV export, and the white-label widget (removes "Powered by answerLoops"). Hard deflection cap. - **Pro** — everything in Standard, plus CSAT scoring, human escalation routing, simulation/dry-run mode, knowledge gap dashboard, and priority support. Soft deflection cap: answers keep going out past 3,000 and the overage is billed at $5 per 100. - **Enterprise** — everything in Pro, plus custom AI model configuration, SLA + dedicated support, and custom invoicing. ## Approaching or hitting your limit Once an org crosses 80% of its monthly deflection allowance, an in-app warning banner appears on **Settings → Billing** with a one-click upgrade to the next tier. What happens at the limit depends on the plan's cap: - **Standard (hard cap)** — AI auto-answering pauses until the next calendar-month reset once 500 deflections are used, and new questions route straight to the human queue instead. There is no Standard overage charge. - **Pro (soft cap)** — answers keep going out past 3,000 deflections. Usage beyond the included amount is billed at $5 per 100 (rounded up to the nearest 100). The Billing page shows how far over the plan the org is. - **Enterprise** — unlimited; there is no cap to reach. ## Self-hosting The core platform is AGPL-3.0 licensed. Self-hosted deployments have no deflection limit and no billing integration required — see [Production Setup Guide](/docs/self-hosting/production). --- # Doc: reference/data-model URL: https://answerloops.com/docs/reference/data-model --- title: Data model description: A map of the workspace, ticket, knowledge, and configuration records. --- This reference describes the main records used by answerLoops. The schema definitions are in `lib/db/schema.ts`; migrations define changes applied to an existing database. ## Workspace and access | Records | Purpose | | --- | --- | | `orgs` | Workspace identity and configuration. | | `users`, `memberships` | User accounts and their workspace roles. | | `invitations` | Pending team invitations. | | `api_keys` | API key metadata and permissions. | ## Support conversations | Records | Purpose | | --- | --- | | `tickets` | Question, source, category, priority, status, and AI draft state. | | `ticket_replies` | Replies associated with a ticket. | | `ticket_events` | Recorded changes in the ticket's history. | | `ticket_links` | Relationships between tickets. | | `ai_assessments` | Results of AI assessment. | | `ticket_feedback` | Positive and negative answer feedback. | | `csat_ratings` | Customer satisfaction ratings. | ## Knowledge | Records | Purpose | | --- | --- | | `kb_sources` | Imported source metadata. | | `kb_articles` | Searchable knowledge articles and their publication state. | | `kb_sync_jobs` | Progress of background imports. | | `ticket_embeddings` | Search representations associated with tickets. | | `faq_snapshots` | Generated FAQ content. | ## Configuration and billing Integration records store channel configuration. `ai_configs` stores workspace model settings. `sla_configs` stores response and resolution targets. `subscriptions` records hosted-plan state, and `api_generations` records standalone answer generation for usage accounting. Use application actions and documented APIs for routine changes. For self-hosted upgrades, apply the documented migrations and retain a tested backup. See [Upgrading](/docs/self-hosting/upgrading). --- # Doc: reference/environment-variables URL: https://answerloops.com/docs/reference/environment-variables --- title: Environment Variables description: Complete reference for every environment variable recognised by answerLoops. --- All variables are set in your `.env` file (local) or in your hosting provider's environment configuration (production). Variables marked **Required** must be present or the app will refuse to start. answerLoops uses **Auth.js v5**. The correct variable names are `AUTH_SECRET` and `AUTH_URL` — **not** `NEXTAUTH_SECRET` / `NEXTAUTH_URL`. If you are migrating from an older deployment, rename them. ## Core (required) | Variable | Description | |---|---| | `DATABASE_URL` | PostgreSQL connection string, e.g. `postgresql://user:pass@host:5432/dbname` | | `DIRECT_DATABASE_URL` | Non-pooled connection string for `LISTEN`/`NOTIFY` only (config hot-reload, live team and dashboard updates). Required whenever `DATABASE_URL` is a pooled connection (Neon `-pooler`, PgBouncer, etc.) — falls back to `DATABASE_URL` otherwise | | `AUTH_SECRET` | Random 32-byte secret used to sign Auth.js v5 session tokens and the OAuth `state` for channel connect flows. Generate with `openssl rand -hex 32` | | `AUTH_URL` | Full public URL of your deployment, e.g. `https://answerloops.com`. Used by Auth.js for OAuth callbacks | | `ENCRYPTION_KEY` | 32-byte hex key used to encrypt stored API keys at rest. Generate with `openssl rand -hex 32`. In production (`NODE_ENV=production`) it is mandatory — saving a credential without it throws instead of storing plaintext | ## Networking | Variable | Description | |---|---| | `TRUST_PROXY_HOPS` | Number of proxies between the public internet and the app. Defaults to `1`. Rate limiters resolve the client IP by counting this many entries in from the right of `x-forwarded-for`, so a caller-supplied prefix on that header can't be used to mint a fresh bucket per request. Set to `2` for Cloudflare in front of a platform load balancer. `cf-connecting-ip` takes precedence when present | | `ORIGIN_VERIFY_SECRET` | Optional. When set, every public pre-auth POST route (`/api/mcp`, `/api/v1/agent/*`, `/api/widget/chat`) rejects any request missing a matching `x-origin-verify` header — without it, the proxy-supplied client-IP header can be spoofed by hitting the origin directly instead of through your edge proxy. Requires a matching rule on your CDN/edge proxy; see the self-hosting guide | ## Multi-domain | Variable | Description | |---|---| | `NEXT_PUBLIC_APP_URL` | Optional. Base URL of a dedicated dashboard subdomain (e.g. `https://app.example.com`) that shares the same deployment as your root domain. When set, marketing-page CTAs link there instead of a relative `/dashboard` path. Unset by default — most deployments serve everything from one domain | | `AUTH_COOKIE_DOMAIN` | Optional. Shares the Auth.js session cookie across every subdomain of your apex (e.g. `.example.com`). Required alongside `NEXT_PUBLIC_APP_URL`, since a session cookie is host-only by default — without it, a user signed in on the root domain appears logged out on the app subdomain | ## OAuth provider Google is the only sign-in provider answerLoops configures. Both variables are required. | Variable | Description | |---|---| | `AUTH_GOOGLE_ID` | Google OAuth client ID | | `AUTH_GOOGLE_SECRET` | Google OAuth client secret | Callback URL to register: `https:///api/auth/callback/google` `DISCORD_CLIENT_ID` below is a separate thing — the "connect a Discord server" flow inside the product, not a sign-in method. There is no Discord or GitHub OAuth provider for dashboard sign-in. ## AI providers These are platform-wide defaults. Individual orgs can override the AI model in **Settings → AI Model**. | Variable | Description | |---|---| | `OPENAI_API_KEY` | OpenAI API key — enables GPT models and OpenAI embeddings | | `ANTHROPIC_API_KEY` | Anthropic API key — enables Claude models | | `GOOGLE_GENERATIVE_AI_API_KEY` | Google AI API key — enables Gemini models | | `GROQ_API_KEY` | Groq API key — enables fast open-weight model inference | | `MISTRAL_API_KEY` | Mistral API key — enables Mistral models | At least one AI provider key is strongly recommended. Without one, AI draft replies are disabled and KB search falls back to keyword matching. ## Widget chat runtime | Variable | Description | |---|---| | `COPILOTKIT_TELEMETRY_DISABLED` | Opts the widget chat's CopilotKit runtime out of its telemetry ping. Defaults to `true`; set to `false` to opt back in. | ## Discord | Variable | Description | |---|---| | `DISCORD_TOKEN` | Bot token from the Discord Developer Portal. Required on both the app service (for the channel picker UI) and the bot service | | `DISCORD_CLIENT_ID` | Discord application client ID. Optional — when set, onboarding offers a 1-click "Add to Discord" flow instead of asking for a manually-created bot token and channel IDs | | `DISCORD_APPLICATION_ID` | Discord application ID (same value as `DISCORD_CLIENT_ID` in most setups). Required to register the `/ask` and `/summarize` slash commands | | `DISCORD_GUILD_ID` | ID of the Discord server to monitor | | `BOT_SECRET` | Shared secret between the bot and the app for authenticating ingest requests | | `BOT_TARGET_URL` | Base URL the bot posts ingested messages to, e.g. `https://answerloops.com` — no trailing slash | ## Slack | Variable | Description | |---|---| | `SLACK_CLIENT_ID` | Slack app client ID | | `SLACK_CLIENT_SECRET` | Slack app client secret | | `SLACK_SIGNING_SECRET` | Used to verify that incoming Slack events are genuine | | `SLACK_POLL_INTERVAL_SECONDS` | How often (in seconds) the app polls Slack for new messages. Defaults to `60` | ## Google Chat | Variable | Description | |---|---| | `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON` | Full service-account JSON key, as a single-line string. Authenticates outgoing replies to the Chat API. | | `GOOGLE_CHAT_ENDPOINT_URL` | Public HTTP endpoint URL for the Chat app's connection settings. Must exactly match what's configured in Google Cloud — used to verify incoming request tokens. | ## GitHub | Variable | Description | |---|---| | `GITHUB_APP_ID` | Numeric ID of your GitHub App | | `GITHUB_APP_PRIVATE_KEY` | PEM-encoded private key generated in the GitHub App settings | | `GITHUB_WEBHOOK_SECRET` | Secret used to verify GitHub webhook payloads. Generate with `openssl rand -hex 32` — paste without trailing newline. Required — `/api/github/webhook` returns `503` until it is set | | `GITHUB_APP_SLUG` | URL slug for your GitHub App, e.g. `answerloops` | ## Telegram | Variable | Description | |---|---| | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather on Telegram | ## Resend (email) | Variable | Description | |---|---| | `RESEND_API_KEY` | Resend API key — used for both outbound transactional email and the email ingest channel | | `RESEND_FROM` | Default sender address for transactional emails, e.g. `support@answerloops.com` | | `RESEND_WAITLIST_FROM` | Sender address for waitlist emails (can be the same as `RESEND_FROM`) | | `RESEND_WEBHOOK_SECRET` | Svix signing secret (`whsec_...`) for Resend's outbound delivery-status webhooks (bounces/complaints) — bounce/complaint tracking is unavailable without it, sending and inbound ingest still work | | `GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET` | Google Cloud OAuth client for the "Connect Gmail" send-only integration — optional, without it that feature is unavailable | | `GMAIL_REDIRECT_URI` | Optional override for the Gmail OAuth callback URL (defaults to `/api/email/gmail/callback`) | | `OUTLOOK_CLIENT_ID` / `OUTLOOK_CLIENT_SECRET` | Microsoft Entra app registration for the "Connect Outlook" send-only integration — optional, without it that feature is unavailable | | `OUTLOOK_REDIRECT_URI` | Optional override for the Outlook OAuth callback URL (defaults to `/api/email/outlook/callback`) | ## Firecrawl | Variable | Description | |---|---| | `FIRECRAWL_API_KEY` | Firecrawl API key — required for KB URL crawl imports | | `FIRECRAWL_API_URL` | Optional. Point at a self-hosted Firecrawl instance instead of the managed service | ## Per-org integrations (no environment variable) Discourse, Circle, and the Notion KB source are configured entirely per-organization — there is no platform-wide app or shared credential to set here. Each org pastes its own token or secret in Settings, encrypted at rest with `ENCRYPTION_KEY` (above). | Integration | Where it's configured | Setup guide | |---|---|---| | Discourse | Integrations → Discourse | [Discourse](/docs/integrations/discourse) | | Circle | Integrations → Circle | [Circle](/docs/integrations/circle) | | Notion KB source | Integrations → Notion | [Notion](/docs/integrations/notion) | ## Analytics tuning | Variable | Description | |---|---| | `ROI_MINUTES_PER_TICKET` | Optional. Minutes assumed saved per deflected ticket, used in the ROI-hours-saved calculation. Defaults to a reasonable estimate — see `lib/analytics/roi.ts` | | `ROI_STAFF_HOURLY_RATE` | Optional. Hourly staff rate used to convert hours saved into a dollar figure. Same defaulting behaviour | ## Web Push (VAPID) | Variable | Description | |---|---| | `VAPID_PUBLIC_KEY` | VAPID public key for browser push notifications | | `VAPID_PRIVATE_KEY` | VAPID private key | | `VAPID_EMAIL` | Contact email included in VAPID headers, e.g. `mailto:admin@answerloops.com` | Generate a VAPID key pair with: ```bash npx web-push generate-vapid-keys ``` ## Sentry (error tracking) | Variable | Description | |---|---| | `SENTRY_DSN` | DSN from your Sentry project's Client Keys settings. Unset disables error tracking entirely — no requests to Sentry are made | | `SENTRY_AUTH_TOKEN` | Optional. Only needed to upload source maps at build time for readable production stack traces | | `SENTRY_ORG` | Optional. Your Sentry organization slug, used alongside `SENTRY_AUTH_TOKEN` | | `SENTRY_PROJECT` | Optional. Your Sentry project slug, used alongside `SENTRY_AUTH_TOKEN` | ## Billing / deployment mode | Variable | Description | |---|---| | `DEPLOYMENT_MODE` | Set to `cloud` **only** on answerLoops' own managed SaaS deployment. Determines whether plan-tier feature gating (Discord/Slack integrations, CSAT scoring, simulation, knowledge gap dashboard, custom AI model config) applies at all. Any other value, or unset, means self-hosted: unmetered, every feature unlocked, no Stripe required | | `STRIPE_SECRET_KEY` | Stripe secret API key. Required when `DEPLOYMENT_MODE=cloud` — its absence there is treated as a misconfiguration (billing shows an error state) rather than falling back to self-hosted/unlimited | | `STRIPE_WEBHOOK_SECRET` | Signing secret for the Stripe webhook endpoint (`/api/billing/webhook`), used to verify subscription lifecycle events | | `STRIPE_PUBLISHABLE_KEY` | Publishable key (`pk_...`) for the embedded checkout form on `/checkout`. Read at request time — deliberately not `NEXT_PUBLIC_`-prefixed, since that prefix is substituted at build time and could never be set by someone running a prebuilt image. Safe to expose in client code by design — it can only create payment attempts, never read or move money. Without it that page shows a disabled state instead of a card form | | `STRIPE_PRICE_STANDARD` / `STRIPE_PRICE_PRO` / `STRIPE_PRICE_ENTERPRISE` | Stripe Price IDs used for each paid plan's checkout session. Each var must hold the price for the plan it names — the amount shown in the app comes from the code, while the amount charged comes from Stripe, so a mismatch bills the wrong figure without erroring | | `STRIPE_PRICE_STANDARD_ANNUAL` / `STRIPE_PRICE_PRO_ANNUAL` / `STRIPE_PRICE_ENTERPRISE_ANNUAL` | Yearly Stripe Price IDs for the same three plans. Selected when a visitor picks annual billing on the pricing page. A plan without one declines annual checkout rather than billing the monthly price. | `DEPLOYMENT_MODE` was introduced to stop deployment type from being inferred from whether `STRIPE_SECRET_KEY` happened to be set — a missing or rotated key on the cloud deployment used to silently degrade every paying org to unmetered/unlimited instead of surfacing as a misconfiguration. --- # Doc: reference/webhooks URL: https://answerloops.com/docs/reference/webhooks --- title: Webhooks description: Find the setup guide for incoming channel and billing events. --- answerLoops receives webhook events from connected platforms. Each integration has its own registration, permissions, and event format; there is no single payload format for all channels. ## Integration guides | Source | Setup and behavior | | --- | --- | | GitHub | [GitHub integration](/docs/integrations/github): Issues, Discussions, comments, and repository synchronization. | | Discourse | [Discourse integration](/docs/integrations/discourse): activity in watched categories. | | Circle | [Circle integration](/docs/integrations/circle): posts and comments in connected spaces. | | Telegram | [Telegram integration](/docs/integrations/telegram): messages delivered to the configured bot. | | Email | [Email integration](/docs/integrations/email): inbound messages and reply configuration. | | Billing | [Stripe integration](/docs/integrations/stripe): subscription events for operators of the hosted service. | ## Verify delivery 1. Complete the integration's setup guide. 2. Send a test message or create the supported event in the connected platform. 3. Check that the expected ticket or status change appears in answerLoops. 4. If nothing appears, check the platform's delivery history and the answerLoops application logs for that event. For self-hosted deployments, confirm that the configured callback URL is reachable over HTTPS and points to the intended deployment. Use the exact route and event types in the integration guide. To build a client that searches knowledge or creates tickets directly, use the [Agent API](/docs/integrations/agent-api) or [MCP](/docs/integrations/mcp). --- # Doc: self-hosting/ai-providers URL: https://answerloops.com/docs/self-hosting/ai-providers --- title: AI Provider Config description: Configure which AI model answerLoops uses for deflection and embeddings. --- answerLoops uses AI for two things: - **Chat** — generating answers to questions - **Embeddings** — converting text to vectors for semantic search ## Platform default Set `OPENAI_API_KEY` in `.env`. This is used by all orgs that haven't configured a custom key in Settings. ```dotenv OPENAI_API_KEY=sk-proj-... ``` ## Per-org keys (via Settings UI) Each org can override the platform key in **Settings → AI Model**. Keys are encrypted at rest using `ENCRYPTION_KEY`. Supported providers: OpenAI, Anthropic, Google Gemini, Groq, Mistral, xAI (Grok), and any OpenAI-compatible endpoint (Ollama, LM Studio, vLLM). Model ID is a live dropdown, not a fixed list. After entering an API key, click **Refresh models** to fetch that provider's current model list directly from its own API — this stays accurate as providers ship new models, instead of a hardcoded list going stale. If the live fetch fails, or before you've run it, Model ID falls back to a small built-in list of recent models, or you can type any model ID directly. ## Embeddings Embeddings use OpenAI's `text-embedding-3-small` by default. This can be changed per-org in Settings → Embeddings. Only OpenAI does embeddings natively. If the chat provider is anything else (Anthropic, Gemini, Groq, Mistral), enter a separate **OpenAI API key** in the embedding key field — the save is rejected without one, and knowledge-base search and every KB import would otherwise fail. An OpenAI-compatible endpoint can serve embeddings too (e.g. Ollama's `nomic-embed-text`). Embedding vectors are stored in the database alongside each KB article. Changing the embedding model requires re-importing all KB articles. ## Self-hosted AI (Ollama) To use Ollama instead of a cloud provider: 1. Run Ollama: `ollama serve` 2. Pull a model: `ollama pull llama3.2` 3. In Settings → AI Model → Chat provider: **OpenAI-compatible** 4. Base URL: `http://localhost:11434/v1` 5. Model ID: `llama3.2` 6. Leave API key blank Ollama runs on the host machine, not inside Docker. Use `http://host.docker.internal:11434/v1` as the base URL when running answerLoops in Docker on Mac/Windows. --- # Doc: self-hosting/discord-bot URL: https://answerloops.com/docs/self-hosting/discord-bot --- title: Discord Bot Setup description: Connect answerLoops to your Discord server — one click on the hosted platform, or full self-hosted control. --- ## Hosted platform (one-click setup) On the answerLoops cloud platform, no Discord Developer Portal is required. 1. Go to **Integrations → Discord** 2. Click **Add answerLoops to Discord** 3. Pick your server in Discord's authorization screen 4. Return to Settings — tick the channels you want monitored 5. Save The platform bot is already running. Channel picks are saved instantly and hot-reloaded — no restart needed. --- ## Self-hosted setup ### 1. Create a Discord Application 1. Go to [discord.com/developers/applications](https://discord.com/developers/applications) 2. Click **New Application** → name it (e.g. "answerLoops") 3. Go to **Bot** tab → **Add Bot** 4. Under **Token** → **Reset Token** → copy it ```dotenv DISCORD_TOKEN=your_bot_token DISCORD_APPLICATION_ID=your_application_id # from General Information tab ``` ### 2. Set bot permissions Under **Bot** tab, enable these **Privileged Gateway Intents**: - Message Content Intent - Server Members Intent (optional, for role pings) ### 3. Invite the bot to your server Go to **OAuth2 → URL Generator**: - Scopes: `bot`, `applications.commands` - Bot permissions: `Send Messages`, `Read Message History`, `View Channels`, `Use Slash Commands` Copy the generated URL and open it in your browser to invite the bot. ### 4. Get channel IDs Enable Developer Mode: Discord → User Settings → Advanced → Developer Mode ✓ Right-click a channel → **Copy Channel ID** ### 5. Configure in answerLoops Settings Go to **Integrations → Discord** and enter: - **Bot Token** — same as `DISCORD_TOKEN` - **Channel IDs** — channels to listen on (comma-separated) - **Escalation Role ID** (optional) — role to @mention when AI confidence is low - **Confidence threshold** — 0–1, default 0.8 --- ## Platform OAuth env vars (self-hosted multi-tenant) To enable the one-click OAuth flow on your own deployment, add these env vars: ```dotenv DISCORD_CLIENT_ID=your_application_client_id DISCORD_CLIENT_SECRET=your_application_client_secret # OAuth2 → Client Secret DISCORD_TOKEN=your_platform_bot_token # single bot shared across all orgs ``` The callback URL to register in Discord OAuth2 is: ``` https://your-domain.com/api/discord/callback ``` ## Config hot-reload The bot uses Postgres `LISTEN/NOTIFY` to pick up config changes instantly — no restart needed. When you save changes in **Integrations → Discord** (channel IDs, bot token, confidence threshold), the database fires a `pg_notify('config_changed')` event. The bot's dedicated listener connection receives it and reloads config within milliseconds. The bot also updates its channel→guild map automatically whenever it joins or leaves a server (`GuildCreate` / `GuildDelete` events) — no manual ID entry required. Both `integrations` (bot token, single-org channel config) and `discord_guilds` (per-guild channel picks for OAuth-connected servers) carry this trigger — a write to either one notifies the bot. The bot maintains one dedicated Postgres connection for `LISTEN`. This is separate from the connection pool and is closed cleanly on `SIGTERM`/`SIGINT`. That connection sends itself a heartbeat query every 4 minutes. On a serverless Postgres provider (e.g. Neon) an idle compute can auto-suspend, silently killing every connection on it — LISTEN included — with no clean disconnect for the bot to react to; a long-idle TCP connection can also get dropped by an intermediate network proxy the same way. The heartbeat keeps the compute active and detects a dead connection quickly. If a heartbeat fails, the LISTEN connection closes, or the initial `LISTEN` fails, the bot reconnects and re-subscribes automatically after a short delay — no manual restart needed to recover config hot-reload. ## Slash commands Once connected, answerLoops registers these slash commands: - `/ask [question]` — search the knowledge base and return the best match - `/summarize` — summarize the current thread as bullet points --- # Doc: self-hosting/docker URL: https://answerloops.com/docs/self-hosting/docker --- title: Docker Setup description: Running answerLoops with Docker Compose. --- Everything here assumes you've already cloned the repo and filled in a real `.env.local` — see the [self-host quickstart](/docs/quickstart-self-host) if you haven't done that part yet. Docker Compose reads that file; it doesn't generate secrets or register OAuth apps for you. ## Run a published image (no build) Official images are published to the GitHub Container Registry, so you can run answerLoops without building anything: ```bash docker compose -f docker-compose.ghcr.yml up -d ``` That pulls `ghcr.io/answerloops/answerloops:latest` and starts both the app and the bot from it. First run takes as long as the download rather than as long as a Next.js build, which on a modest machine is the difference between a minute and quarter of an hour. Images are built for `linux/amd64` and `linux/arm64`, so Apple Silicon runs natively rather than under emulation. ### Pinning a version `latest` moves when a new release is published. For anything you rely on staying put, pin a version instead: ```bash ANSWERLOOPS_IMAGE=ghcr.io/answerloops/answerloops:1.4.0 \ docker compose -f docker-compose.ghcr.yml up -d ``` Available tags are listed on the [package page](https://github.com/answerLoops/answerLoops/pkgs/container/answerloops). Alongside each release there is a `main` tag built from the tip of the default branch, and a `sha-` tag for pinning to an exact commit. ### Which file to use | | | |---|---| | `docker-compose.yml` | local development — builds from source, includes Postgres | | `docker-compose.ghcr.yml` | self-hosting — pulls a published image, brings your own Postgres | | `docker-compose.prod.yml` | self-hosting from source — builds locally, brings your own Postgres | The published-image path expects `DATABASE_URL` in `.env` to point at a Postgres you run: the compose file deliberately does not start one, because a database inside the same compose project is convenient for development and the wrong default for anything holding real data. Both services run the same image and differ only in their command, so there is no second image to keep in step. ## Start ```bash docker compose up --build ``` On first run this: 1. Builds the Next.js app image 2. Starts Postgres with a named volume (`postgres-data`) for persistence 3. Runs Drizzle migrations automatically 4. Serves the app on `http://localhost:3000` ## Stop (keep data) ```bash docker compose down ``` Never run `docker compose down -v`. The `-v` flag deletes named volumes, which includes your Postgres data. All articles, tickets, and org settings will be lost. ## Rebuild after code changes ```bash docker compose up --build ``` ## View logs ```bash # All services docker compose logs -f # App only docker compose logs app -f # Postgres only docker compose logs postgres -f ``` ## Access the database ```bash docker compose exec postgres psql -U community -d community ``` Common queries: ```sql -- List all tables \dt -- Check org onboarding status SELECT id, name, onboarded_at FROM orgs; -- Check users SELECT id, email, provider FROM users; -- Exit \q ``` ## Named volumes | Volume | Contents | |---|---| | `postgres-data` | All database data | | `pnpm-store` | pnpm package cache (speeds up rebuilds) | ## Environment variables in Docker Docker Compose reads from your `.env` file automatically. Any variable set in `.env` is available inside the `app` container. To verify a variable is loaded: ```bash docker compose exec app printenv OPENAI_API_KEY ``` --- # Doc: self-hosting/email-channel URL: https://answerloops.com/docs/self-hosting/email-channel --- title: Email Channel Setup description: Route inbound support emails through the answerLoops AI pipeline. --- ## How it works Email doesn't poll an inbox. In the hosted flow, answerLoops verifies a customer-owned domain with Resend receiving; Resend posts signed `email.received` events to the webhook, answerLoops retrieves the full message, and the AI pipeline triages, embeds, and answers it. Replies go directly to the sender via Resend with proper RFC 5322 threading (`Message-ID` / `In-Reply-To` / `References`). For self-hosted deployments, you can use the same Resend receiving flow or keep an existing provider that can POST inbound messages to the webhook with the per-organization legacy secret. ## Setup ### 1. Prerequisites - `RESEND_API_KEY` and `RESEND_FROM` must be set (used for outbound replies) - A Resend domain configured with sending and receiving capabilities, or a supported inbound provider for the legacy path ### 2. Configure in answerLoops Settings Under **Integrations → Email**, enter: - **Allowed sender addresses/domains** (optional) — comma-separated list of emails or domains to accept (e.g. `example.com, partner@other.com`). Leave blank to accept all inbound email. - **Escalation email** (optional) — referenced in replies when AI confidence is below threshold. - **Confidence threshold** — 0–1, default 0.8. Choose **Use your own domain** and add the DKIM, return-path/SPF, and inbound MX records shown in Settings. Once the domain is verified, customers send mail to `support@yourdomain`. ### 3. Configure your receiving webhook Point your provider's inbound webhook at: ``` POST https://yourapp.com/api/email/ingest ``` Resend sends signed `email.received` events to that endpoint. Set `RESEND_WEBHOOK_SECRET` to the signing secret from Resend. The route uses the recipient domain to find the organization, then retrieves the full message with `RESEND_API_KEY`. For a legacy provider, set the custom secret header: ``` X-Email-Webhook-Secret: ``` #### Provider-specific guides **SendGrid Inbound Parse** 1. Settings → Inbound Parse → Add Host & URL 2. Enter your domain and `https://yourapp.com/api/email/ingest` 3. Set the `X-Email-Webhook-Secret` header via your SendGrid HTTP POST settings **Mailgun Routes** 1. Sending → Routes → Create Route 2. Match filter: `match_recipient("support@yourdomain.com")` 3. Action: `forward("https://yourapp.com/api/email/ingest")` 4. Add `X-Email-Webhook-Secret` in the route headers **Postmark Inbound** 1. Message Streams → Inbound → Settings 2. Inbound webhook URL: `https://yourapp.com/api/email/ingest` 3. Set the `X-Email-Webhook-Secret` header in Postmark webhook settings **Cloudflare Email Routing** 1. Email → Email Routing → Rules 2. Forward to a Worker that POSTs to your webhook URL with the secret header ## Verify Send an email to your address. A ticket should appear in `/tickets` within a few seconds with source badge "Email from sender@domain.com". Reply to the AI's answer from your own inbox — it should append to the same ticket rather than opening a new one. ## Reliability A few things worth knowing about how the pipeline handles the messy realities of email: - **Idempotent by construction.** Every inbound email is keyed on its RFC `Message-ID`, so a provider webhook retry is a no-op, never a duplicate ticket. - **Mail-loop guarded.** `Auto-Submitted`, `Precedence`, `X-Auto-Response-Suppress`, `List-Id`, and no-reply sender patterns are all detected and rejected before a reply is ever generated. A per-sender reply throttle backstops anything that slips through. - **Spam-tolerant, fail-open.** Provider spam signals are honored when present, but their absence never blocks a legitimate email. - **HTML fallback.** HTML-only emails are converted to plain text rather than silently dropped. - **Delivery-status visibility.** Bounces, spam complaints, and delivery failures on outbound replies are tracked against the ticket and surfaced as a badge on the ticket detail page. - **Rate-limited per org.** A compromised or misbehaving upstream account can't burn one org's AI spend or flood the ticket queue for everyone else. ## Custom domain (verified sending) An org can verify a domain it owns so outbound replies send with that domain in `From:` instead of the platform default — see [Custom domain (verified sending)](/docs/integrations/email#custom-domain-verified-sending) for the customer-facing flow. Under the hood this uses Resend's Domains API (`domains.create`/`get`/`remove`), backed by an `email_domains` table (one verified domain per org for v1) and an `integrations.email_send_method` discriminator (`'platform' | 'oauth' | 'domain'`) that `lib/email/reply.ts` branches on when choosing the `From:` address. `RESEND_API_KEY` needs Domains API access in addition to Sending — no separate env var is required for this feature beyond that existing key. ## Connect Gmail (send-only OAuth) An org can connect its own Gmail mailbox so outbound replies send through it directly instead of the platform default or a verified domain — see [Connect Gmail](/docs/integrations/email#connect-a-mailbox-gmail-or-outlook-send-only-oauth) for the customer-facing flow. Requires a Google Cloud OAuth client requesting only the `gmail.send` scope (a *sensitive*, not *restricted*, scope — it triggers Google's standard OAuth consent-screen review, not the multi-week CASA security assessment; still submit for review as soon as the client/consent screen exists if you plan to let real external users connect). An `email_oauth_connections` table stores the encrypted access/refresh token pair (one connection per org for v1); `integrations.email_send_method`'s `'oauth'` value routes `lib/email/reply.ts` through `lib/email/gmail.ts` instead of Resend. Token refresh is handled automatically on send; a dead refresh token (password change, admin revocation, ~6 months inactivity) is detected reactively on the next send attempt, flips the connection to disconnected, and emails the org's admins the same day with a reconnect link — replies fall back to the platform default in the meantime rather than failing silently. **Manual setup required** (this is not automatic): create a Google Cloud project, configure an OAuth consent screen, create OAuth 2.0 credentials requesting the `gmail.send` scope, and add the app's `/api/email/gmail/callback` URL as an authorized redirect URI. Submit the consent screen for Google's review before external users (not just your own testing) can connect. ## Connect Outlook (send-only OAuth) The Microsoft equivalent of the Gmail flow above — same `email_oauth_connections` table, same `integrations.email_send_method` discriminator, routed through `lib/email/outlook.ts` instead. Requires a Microsoft Entra app registration requesting only delegated `Mail.Send` (`offline_access` alongside it, to get a refresh token). At most one OAuth mailbox connection can exist per org — connecting Outlook while Gmail is connected (or vice versa) replaces it, since `email_oauth_connections.orgId` is unique. **Manual setup required**: register an app in Microsoft Entra ID, add a client secret, configure the delegated `Mail.Send` and `offline_access` API permissions, and add the app's `/api/email/outlook/callback` URL as a redirect URI under the app's Authentication settings. **Microsoft additionally requires Partner Center publisher verification** before real external users (not just your own tenant) can consent without hitting a step-up-consent block — this is a separate, slower, account-level process from just registering the app. Start it early if you plan to offer this to real customers; internal testing against your own tenant works without it. Unlike Gmail's send path (which mints its own RFC `Message-ID` and sets `In-Reply-To`/`References` directly), Microsoft Graph's sending API doesn't support setting standard headers on outbound mail the same way — `lib/email/outlook.ts` creates a draft, sends it, then reads back the real `Message-ID` Graph assigned for answerLoops's own threading records. `In-Reply-To` is set best-effort via a MAPI extended property; `References` has no equivalent and is not sent. This is a deliberate, documented per-provider difference, not a bug. ## Environment variables | Variable | What it is | |---|---| | `RESEND_API_KEY` | Required for outbound replies and for the custom-domain verification flow (Domains API access) | | `RESEND_FROM` | Default reply-from address (e.g. `support@yourdomain.com`) | | `RESEND_WEBHOOK_SECRET` | Svix signing secret for Resend's outbound delivery-status webhooks (`whsec_...`, `email.bounced`/`email.complained`/etc.) — bounce/complaint tracking is unavailable without it, but sending and inbound ingest both still work | | `GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET` | Google Cloud OAuth client credentials for the Connect Gmail flow. Without these, that feature is unavailable but nothing else breaks | | `GMAIL_REDIRECT_URI` | Optional override for the OAuth callback URL; defaults to `/api/email/gmail/callback` | | `OUTLOOK_CLIENT_ID` / `OUTLOOK_CLIENT_SECRET` | Microsoft Entra app registration credentials for the Connect Outlook flow. Without these, that feature is unavailable but nothing else breaks | | `OUTLOOK_REDIRECT_URI` | Optional override for the OAuth callback URL; defaults to `/api/email/outlook/callback` | The legacy inbound webhook secret is generated automatically and stored per organization. Hosted customers do not configure it; Resend receiving uses the platform webhook signing secret instead. ## Notes - Quoted reply chains are stripped — only the new message content is ingested. - `Subject` is prepended to the body so triage has full context. - Messages under 10 characters are ignored. - Replies set `Message-ID`, `In-Reply-To`, and `References` for correct email-client threading. --- # Doc: self-hosting/enterprise URL: https://answerloops.com/docs/self-hosting/enterprise --- title: Enterprise deployment description: Review deployment, integration permissions, and operational requirements. --- Use this guide to prepare an internal review of a self-hosted answerLoops deployment. The integrations you select determine its network and platform-permission requirements. ## Application and data services Plan the application domain, authentication, database, storage, backups, and upgrade process. Your team operates these services in a self-hosted deployment. See [Production](/docs/self-hosting/production). Application hosting and model processing are separate choices. External chat and embedding providers receive relevant content, and connected messaging platforms continue to process conversations. ## Channel connection methods | Channel | Connection method | | --- | --- | | Discord | A bot uses the platform gateway and API. | | Slack | Events API, or polling mode for self-hosted deployments. | | GitHub | GitHub App webhooks. | | Telegram | A bot webhook. | | Email | An inbound-message endpoint and configured reply service. | | Google Chat | A configured Chat app endpoint and paired space. | | Discourse | API access and webhook configuration. | | Circle | API access for selected spaces and comments. | Follow each [integration guide](/docs/integrations/discord) for the required permissions and callback configuration. Installation approval depends on the connected workspace's policies; self-hosting does not override them. ## Slack polling Polling is an alternative to Slack event delivery when operating a self-hosted deployment. The bot requests channel history at the configured interval. This changes how Slack events reach the bot; it does not remove the application's other network requirements. See [Slack app setup](/docs/self-hosting/slack-app) for scopes, channel access, and polling configuration. ## Prepare for launch - Assign owners for application operations, support review, and source documentation. - Verify login, channel delivery, and replies with test conversations. - Check chat and embedding access from the deployed services. - Set backup and restore procedures and plan application updates. - Document the data-processing and service requirements for your organization. For hosted Enterprise, contact [hello@answerloops.com](mailto:hello@answerloops.com) to discuss model configuration, migration assistance, service levels, and agreement requirements. --- # Doc: self-hosting/environment-variables URL: https://answerloops.com/docs/self-hosting/environment-variables --- title: Environment Variables description: Every environment variable answerLoops uses, with defaults and requirements. --- Copy `.env.example` to `.env` and fill in the values below. answerLoops uses **Auth.js v5**. The required env vars are `AUTH_URL` and `AUTH_SECRET`. If you see `NEXTAUTH_URL` or `NEXTAUTH_SECRET` in older docs or guides, those are the v4 names — they will not work here. ## Core (required) | Variable | Description | Example | |---|---|---| | `DATABASE_URL` | PostgreSQL connection string | `postgresql://community:community@postgres:5432/community` | | `DIRECT_DATABASE_URL` | Non-pooled connection string, used only for `LISTEN`/`NOTIFY` (config hot-reload, live team updates, live dashboard updates). **Required if `DATABASE_URL` goes through a connection pooler** (Neon's `-pooler` endpoint, PgBouncer, Supabase's pooled port) — pooled connections don't reliably deliver `NOTIFY` to a `LISTEN`er, since the pooler can swap the physical backend between statements. Falls back to `DATABASE_URL` if unset, which is correct for a plain unpooled Postgres instance (e.g. the local `docker compose` Postgres) but silently breaks hot-reload on a pooled provider. | Neon: the same connection string with `-pooler` removed from the hostname (Neon dashboard → Connection Details → toggle "Pooled connection" off) | | `AUTH_URL` | Full public URL of your deployment | `http://localhost:3000` or `https://app.example.com` | | `AUTH_SECRET` | Random 32-byte hex — signs JWT sessions and the OAuth `state` for channel connect flows | `openssl rand -hex 32` | | `ENCRYPTION_KEY` | Random 32-byte hex — encrypts stored API keys and bot tokens at rest. **Required in production**: with `NODE_ENV=production` and this unset, saving any credential throws rather than writing it in cleartext. | `openssl rand -hex 32` | ## Networking (optional) | Variable | Description | Example | |---|---|---| | `TRUST_PROXY_HOPS` | How many proxies sit between the public internet and the app. Defaults to `1` | `1` | Rate limiting needs the real client IP, and it reads that from `x-forwarded-for`. That header is a list each proxy appends to, so its *leftmost* entry is whatever the caller claimed — trusting it lets anyone mint a fresh rate-limit bucket per request just by rotating a header value. answerLoops instead counts in from the right by `TRUST_PROXY_HOPS`, reading the entry your own infrastructure appended. ## Multi-domain (optional) Only relevant if you're running the dashboard on a separate subdomain from your marketing/root domain (e.g. `app.example.com` alongside `example.com`) while both point at the same deployment. Most self-hosted setups use a single domain and can skip this section entirely. | Variable | Description | Example | |---|---|---| | `NEXT_PUBLIC_APP_URL` | Base URL of the dashboard subdomain. When set, marketing-page CTAs link there instead of a relative `/dashboard` path. Leave unset for a single-domain deployment. | `https://app.example.com` | | `AUTH_COOKIE_DOMAIN` | Shares the session cookie across every subdomain of your apex domain. Required alongside `NEXT_PUBLIC_APP_URL` — without it, a user signed in on the root domain appears logged out the moment they land on the app subdomain, since a session cookie is host-only by default. Leave unset for a single-domain deployment. | `.example.com` | With `NEXT_PUBLIC_APP_URL` set, `auth.ts` redirects any request for a path *not* listed in its `WEBSITE_PATHS`/`PUBLIC_PATHS` arrays from the root domain over to the app subdomain — that's how a dashboard link typed on the marketing domain still lands in the app. Every marketing page you add (a new landing page, comparison page, etc.) must be added to both arrays, or it 307s off the marketing domain to the app subdomain's login screen instead of rendering — unreachable to visitors and to search crawlers alike. Set this to the actual number of proxies in front of the app. Too high and you read a value the client controls, which defeats per-IP limiting. Too low and everyone behind your edge shares one bucket, which throttles legitimate traffic. The default of `1` is correct for a single load balancer or CDN (Railway, Fly, a lone nginx). Add one for each additional layer — Cloudflare in front of Railway is `2`. Cloudflare's `cf-connecting-ip` header is used when present and takes precedence, since Cloudflare overwrites rather than appends it. | Variable | Description | Example | |---|---|---| | `ORIGIN_VERIFY_SECRET` | Optional secret that locks the proxy-supplied client-IP header's trust to traffic that actually passed through your edge proxy/CDN | `openssl rand -hex 32` | The proxy-supplied client-IP header (`cf-connecting-ip` or equivalent) is only spoof-proof if the origin is unreachable except through your edge proxy. If your origin is still reachable directly, anyone can set that header themselves and mint a fresh rate-limit bucket per request, bypassing both the per-IP limiter and your edge's own WAF/DDoS layer. To close this: 1. Generate a secret: `openssl rand -hex 32`. 2. Set `ORIGIN_VERIFY_SECRET` to that value in your deployment's environment. 3. On your CDN/edge proxy, add a rule (most offer a "modify request header" or equivalent feature) that runs for all traffic and sets a request header `x-origin-verify` to the same secret value. 4. Confirm requests hitting your origin without that header now get a `403 Forbidden` from `/api/mcp`, `/api/v1/agent/*`, and `/api/widget/chat`. Until this is configured, the proxy-supplied client-IP header is trusted unconditionally — the app has no way to tell an edge-routed request from a direct one. Set `ORIGIN_VERIFY_SECRET` on any deployment sitting behind a CDN or edge proxy. ## OAuth (required) Login is Google OAuth only. | Variable | Description | |---|---| | `AUTH_GOOGLE_ID` | Google OAuth client ID | | `AUTH_GOOGLE_SECRET` | Google OAuth client secret | Callback URL to register: `{AUTH_URL}/api/auth/callback/google` `DISCORD_CLIENT_ID` and `GITHUB_APP_ID` below are unrelated to sign-in — they're the "connect a Discord server" and "install the GitHub App" flows inside the product, available once you're already logged in with Google. ## AI providers (optional — at least one recommended) Per-org AI keys set in **Settings → AI Model** override these env vars for that org. The platform key is the fallback when no org key is set. | Variable | Provider | |---|---| | `OPENAI_API_KEY` | OpenAI (covers all features including embeddings) | | `ANTHROPIC_API_KEY` | Anthropic platform default | | `GOOGLE_GENERATIVE_AI_API_KEY` | Google Gemini platform default | | `GROQ_API_KEY` | Groq platform default | | `MISTRAL_API_KEY` | Mistral platform default | ## Widget chat runtime (optional) | Variable | Description | |---|---| | `COPILOTKIT_TELEMETRY_DISABLED` | Opts the widget chat's CopilotKit runtime out of its telemetry ping. Defaults to `true` (set in `instrumentation.ts` before any route loads) — set it to `false` yourself if you want that telemetry on. | ## Discord (optional) | Variable | Description | |---|---| | `DISCORD_TOKEN` | Platform shared bot token. Required for 1-click OAuth mode and for the channel picker in Settings. | | `DISCORD_CLIENT_ID` | Application ID — enables the "Add to Discord" 1-click flow from onboarding. Register callback: `{AUTH_URL}/api/discord/callback`. Without it, onboarding falls back to a manual bot-token + channel-ID flow | | `DISCORD_APPLICATION_ID` | Application ID for slash command registration (same value as `DISCORD_CLIENT_ID`) | | `DISCORD_GUILD_ID` | Guild ID for guild-scoped slash command registration during dev (instant vs. global's 1-hour delay) | | `BOT_SECRET` | Shared secret between the bot service and `/api/ingest`. Auto-generated per-org via Settings; this is the env fallback. | | `BOT_TARGET_URL` | URL of the app service as seen from the bot container. **No trailing slash or period.** Default: `http://localhost:3000` | `DISCORD_TOKEN` must be set on **both** the app service and the bot service. The app service uses it to power the channel picker in Integrations → Discord. The bot service uses it to connect to Discord's gateway. If they reference different Discord applications, the bot joins a different server than users authorized. ## Slack (optional) | Variable | Description | |---|---| | `SLACK_CLIENT_ID` | OAuth app Client ID. Enables 1-click "Add to Slack" install. | | `SLACK_CLIENT_SECRET` | OAuth app Client Secret. Required with `SLACK_CLIENT_ID`. | | `SLACK_SIGNING_SECRET` | Signing secret — verifies Events API webhook payloads. | | `SLACK_POLL_INTERVAL_SECONDS` | Poll interval in polling mode. Default: `60`. Minimum: `30`. | Add `{AUTH_URL}/api/slack/callback` to your Slack app's OAuth Redirect URLs. ## Google Chat (optional) | Variable | Description | |---|---| | `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON` | Full service-account JSON key, as a single-line string. Used to authenticate outgoing replies to the Chat API. | | `GOOGLE_CHAT_ENDPOINT_URL` | Public HTTP endpoint URL, e.g. `https://{YOUR_DOMAIN}/api/google-chat/events`. Must exactly match the endpoint URL configured in the Chat app — Google verifies each request's token audience against this value. | See [Google Chat App Setup](/docs/self-hosting/google-chat-app) for the one-time Google Cloud configuration these values come from. ## GitHub App (optional) | Variable | Description | |---|---| | `GITHUB_APP_ID` | App ID from GitHub App settings page | | `GITHUB_APP_PRIVATE_KEY` | Base64-encoded PEM: `base64 -i your-app.pem \| tr -d '\n'` | | `GITHUB_WEBHOOK_SECRET` | Webhook secret. **Generate with `openssl rand -hex 32` and paste without the trailing newline.** Required: `/api/github/webhook` returns `503` and processes no deliveries until it is set, and the value must match the secret configured on the GitHub App. | | `GITHUB_APP_SLUG` | URL-safe app name (slug) — shown in your GitHub App's URL | Register these URLs in your GitHub App: - Callback URL: `{AUTH_URL}/api/github/callback` - Webhook URL: `{AUTH_URL}/api/github/webhook` Subscribe to events: Issues, Issue comments, Discussions, Discussion comments, Push. ## Telegram (optional) | Variable | Description | |---|---| | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather. Fallback when no org token is saved. | After deploying, register the webhook: `POST {AUTH_URL}/api/telegram/register` (or click **Register webhook** in Integrations → Telegram). ## Email notifications — Resend (optional) | Variable | Description | |---|---| | `RESEND_API_KEY` | API key from resend.com. Enables email alerts (new ticket, SLA breach, resolved). | | `RESEND_FROM` | Verified sender address e.g. `hello@yourdomain.com` | | `RESEND_WAITLIST_FROM` | From address for waitlist confirmation emails. Falls back to `RESEND_FROM`. | | `RESEND_WEBHOOK_SECRET` | Svix signing secret (`whsec_...`) for Resend's outbound delivery-status webhooks (`email.bounced`/`email.complained`/etc.) — see [Email Channel Setup](/docs/self-hosting/email-channel) | | `GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET` | Google Cloud OAuth client credentials for the "Connect Gmail" send-only integration (see [Email Channel Setup](/docs/self-hosting/email-channel)) — optional, requires manual GCP OAuth-client + consent-screen setup | | `GMAIL_REDIRECT_URI` | Optional override for the Gmail OAuth callback URL (defaults to `/api/email/gmail/callback`) | | `OUTLOOK_CLIENT_ID` / `OUTLOOK_CLIENT_SECRET` | Microsoft Entra app registration credentials for the "Connect Outlook" send-only integration (see [Email Channel Setup](/docs/self-hosting/email-channel)) — optional, requires manual Entra app registration + (for external users) Partner Center publisher verification | | `OUTLOOK_REDIRECT_URI` | Optional override for the Outlook OAuth callback URL (defaults to `/api/email/outlook/callback`) | If absent, email notifications skip silently — nothing breaks. Without `RESEND_WEBHOOK_SECRET`, bounce/complaint tracking on outbound replies is unavailable, but sending and inbound ingest both still work. Without `GMAIL_CLIENT_ID`/`GMAIL_CLIENT_SECRET` or `OUTLOOK_CLIENT_ID`/`OUTLOOK_CLIENT_SECRET`, the respective Connect button is unavailable but every other email path still works. ## URL ingest — Firecrawl (optional) | Variable | Description | |---|---| | `FIRECRAWL_API_KEY` | API key from firecrawl.dev. Enables Settings → Import from URL to crawl docs sites into the KB. | ## Discourse No environment variable. Each org connects its own Discourse forum in **Integrations → Discourse** by pasting a bot-scoped API key, which is stored encrypted — make sure `ENCRYPTION_KEY` (above) is set before connecting in any real deployment. See [Discourse](/docs/integrations/discourse) for the full setup. ## Circle No environment variable. Each org connects its own Circle community in **Integrations → Circle** using a per-org webhook secret — make sure `ENCRYPTION_KEY` (above) is set before connecting in any real deployment. See [Circle](/docs/integrations/circle) for the full setup. ## Notion KB source No environment variable. Each org connects its own Notion workspace in **Integrations → Notion** by pasting an internal integration token, which is stored encrypted — make sure `ENCRYPTION_KEY` (above) is set before connecting in any real deployment. See [Notion](/docs/integrations/notion) for the full setup. ## Web push notifications (optional) | Variable | Description | |---|---| | `VAPID_PUBLIC_KEY` | VAPID public key | | `VAPID_PRIVATE_KEY` | VAPID private key | | `VAPID_EMAIL` | `mailto:you@example.com` — identifies your push endpoint | Generate keys: ```bash pnpm dlx web-push generate-vapid-keys ``` ## Error tracking — Sentry (optional) | Variable | Description | |---|---| | `SENTRY_DSN` | DSN from your Sentry project's Client Keys settings. Unset means error tracking is fully disabled — no requests to Sentry are made. | | `SENTRY_AUTH_TOKEN` | Only needed to upload source maps at build time, for readable stack traces. Unset means builds succeed without uploading maps. | | `SENTRY_ORG` | Your Sentry organization slug. Only used alongside `SENTRY_AUTH_TOKEN`. | | `SENTRY_PROJECT` | Your Sentry project slug. Only used alongside `SENTRY_AUTH_TOKEN`. | ## Billing / deployment mode | Variable | Description | Example | |---|---|---| | `DEPLOYMENT_MODE` | Set to `cloud` only on answerLoops' own managed SaaS. Leave unset (or anything other than `cloud`) on a self-hosted deployment. | `cloud` | | `STRIPE_SECRET_KEY` | Only relevant when `DEPLOYMENT_MODE=cloud`. Not needed for self-hosting. | — | | `STRIPE_WEBHOOK_SECRET` | Only relevant when `DEPLOYMENT_MODE=cloud`. Signing secret for `/api/billing/webhook`. | — | | `STRIPE_PUBLISHABLE_KEY` | Only relevant when `DEPLOYMENT_MODE=cloud`. Publishable key for the embedded checkout form, read at request time. Not `NEXT_PUBLIC_`-prefixed on purpose: that prefix is baked in at build time. Safe in client code — it can only create payment attempts. | `pk_live_...` | | `STRIPE_PRICE_STANDARD` / `STRIPE_PRICE_PRO` / `STRIPE_PRICE_ENTERPRISE` | Only relevant when `DEPLOYMENT_MODE=cloud`. Monthly Stripe Price ID per plan — must match the plan it names, since the displayed price and the charged price come from different places. | — | | `STRIPE_PRICE_STANDARD_ANNUAL` / `STRIPE_PRICE_PRO_ANNUAL` / `STRIPE_PRICE_ENTERPRISE_ANNUAL` | Only relevant when `DEPLOYMENT_MODE=cloud`. Annual Stripe Price ID per plan. A plan missing its annual price declines annual checkout rather than billing the monthly rate. | — | Self-hosted deployments (the default — `DEPLOYMENT_MODE` unset) are never metered and every plan-gated feature (Discord/Slack integrations, CSAT scoring, simulation/dry-run mode, knowledge gap dashboard, custom AI model config, etc.) is unlocked unconditionally. You're already bringing your own AI provider, database, and hosting — there's no usage of yours for answerLoops to meter or restrict. Don't set `DEPLOYMENT_MODE=cloud` on a self-hosted instance — it opts the deployment into plan-tier gating with no Stripe subscription behind it, which locks every gated feature instead of unlocking them. --- # Doc: self-hosting/google-chat-app URL: https://answerloops.com/docs/self-hosting/google-chat-app --- title: Google Chat App Setup description: One-time Google Cloud configuration to enable Google Chat as a support channel. --- This is a platform-operator, one-time setup — done once for your whole deployment, not per connected org. Individual orgs then connect via the connect-code flow in [Google Chat integration docs](/docs/integrations/google-chat). ## 1. Create a Google Cloud project (or reuse an existing one) Go to the [Google Cloud Console](https://console.cloud.google.com) and create a project, or reuse one you already have. ## 2. Enable the Google Chat API **APIs & Services → Library** → search **Google Chat API** → **Enable**. ## 3. Create a service account **IAM & Admin → Service Accounts → Create Service Account**. No special IAM roles are required for basic messaging — the Chat API authorizes based on the Chat app configuration, not project-level IAM roles. Create a JSON key for this service account and keep it safe — it becomes `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON`. ## 4. Configure the Chat app **Google Chat API → Configuration**: - **App name / avatar / description** — however you want the bot to appear - **Functionality** — enable "Receive 1:1 messages" and "Join spaces and group conversations" - **Connection settings** → **HTTP endpoint URL** → your public endpoint: ``` https://{YOUR_DOMAIN}/api/google-chat/events ``` - **Visibility** — for an unlisted app usable outside your own Google Workspace, list the specific domains or individual users/groups allowed to install it. A Workspace admin on the *installing* side may also need to enable third-party Chat apps for their domain (their **Admin Console → Apps → Google Workspace Marketplace apps** settings) before the app can be added to a space at all. ## 5. Set environment variables ```bash GOOGLE_CHAT_SERVICE_ACCOUNT_JSON='{"type":"service_account","project_id":"...", ...}' # the full JSON key, as a single-line string GOOGLE_CHAT_ENDPOINT_URL=https://{YOUR_DOMAIN}/api/google-chat/events ``` `GOOGLE_CHAT_ENDPOINT_URL` must exactly match the HTTP endpoint URL configured in step 4 — Google signs each incoming request's token audience to that exact value, and verification fails on any mismatch (trailing slash included). ## 6. Connect an org Once the above is live, any org can connect from **Settings → Integrations → Google Chat** — see [Google Chat integration docs](/docs/integrations/google-chat) for the connect-code flow. ## Distribution note This setup makes the app installable as an **unlisted app** — fast to stand up, but each customer's Workspace admin has a manual step (enabling third-party apps, then adding this one to a space). A [Google Workspace Marketplace](https://developers.google.com/workspace/marketplace/about-app-review) listing would let a customer install with a couple of clicks and no admin-console detour first, at the cost of a one-time Google review (OAuth verification + listing review, commonly a few weeks) before it goes live. Both use the same `/api/google-chat/events` endpoint and env vars — a later Marketplace listing wouldn't require re-doing this setup. Marketplace submission asks for a public privacy policy URL as part of the app configuration and OAuth verification — the hosted deployment already has one at `/privacy`. --- # Doc: self-hosting/oauth URL: https://answerloops.com/docs/self-hosting/oauth --- title: OAuth Setup description: Configure GitHub and Google login for your self-hosted instance. --- answerLoops uses OAuth for authentication. You need at least one provider. ## GitHub OAuth 1. Go to [github.com/settings/developers](https://github.com/settings/developers) → **OAuth Apps** → **New OAuth App** 2. Fill in: - **Application name**: answerLoops (or anything) - **Homepage URL**: `http://localhost:3000` (or your production URL) - **Authorization callback URL**: `http://localhost:3000/api/auth/callback/github` 3. Click **Register application** 4. Copy the **Client ID** and generate a **Client Secret** Add to `.env`: ```dotenv AUTH_GITHUB_ID=your_client_id AUTH_GITHUB_SECRET=your_client_secret ``` ## Google OAuth 1. Go to [console.cloud.google.com](https://console.cloud.google.com) → **APIs & Services** → **Credentials** 2. Click **Create Credentials** → **OAuth client ID** 3. Application type: **Web application** 4. Add authorized redirect URI: `http://localhost:3000/api/auth/callback/google` 5. Copy the **Client ID** and **Client Secret** Add to `.env`: ```dotenv AUTH_GOOGLE_ID=your_client_id AUTH_GOOGLE_SECRET=your_client_secret ``` ## Production callback URLs Replace `http://localhost:3000` with your production domain in all callback URLs. OAuth callback URLs must match exactly — trailing slashes, http vs https, and port numbers all matter. --- # Doc: self-hosting/prerequisites URL: https://answerloops.com/docs/self-hosting/prerequisites --- title: Prerequisites description: What you need before running answerLoops on your own infrastructure. --- Gather these before you start the [self-host quickstart](/docs/quickstart-self-host) — nothing below takes long to get, but bouncing between OAuth app dashboards mid-setup is the most common source of "why isn't this working." ## Required | Requirement | Version | Notes | |---|---|---| | Node.js | 22+ | Required by Next.js 16 and pnpm | | Docker | Latest stable | Includes Docker Compose v2 | | pnpm | 11+ | Used for package management | | Git | Any | For cloning the repo | | Postgres with the `pgvector` extension | 15+ | `docker-compose.yml` already runs `pgvector/pgvector:pg16`, so nothing to do for the Docker path. Bringing your own Postgres (Neon, Supabase, RDS, a bare install) instead of the provided compose file? Run `CREATE EXTENSION vector;` once before starting the app — every major managed provider supports this natively. Powers KB search's vector index. | ## Required accounts / keys | Service | Purpose | Free tier? | |---|---|---| | GitHub **or** Google | OAuth login for your team | Yes | | OpenAI (or other AI provider) | AI deflection + KB embeddings | Pay per use | ## Optional (add channels as you need them) You don't need all of these to get started — connect one channel first, confirm it works, then add the rest. | Service | Purpose | |---|---| | Discord | Community bot integration | | Slack | Community bot integration | | GitHub App | Issue/Discussion ingestion + KB sync | ## Generating secrets Several env vars require random secrets. Generate them with: ```bash # AUTH_SECRET and ENCRYPTION_KEY openssl rand -hex 32 ``` Run this twice — once for each secret. Never reuse the same value for both. `ENCRYPTION_KEY` encrypts API keys stored in the database. If you change it after setup, all stored API keys become unreadable and users will need to re-enter them. --- # Doc: self-hosting/production URL: https://answerloops.com/docs/self-hosting/production --- title: Production Deployment description: Deploy answerLoops to a production server. --- This assumes you've already worked through the [self-host quickstart](/docs/quickstart-self-host) locally and know your setup works — production is the same app, just with real secrets, a real domain, and `docker-compose.prod.yml` instead of the dev compose file. ## Recommended stack - **Server**: Any Linux VPS (Ubuntu 22.04+) - **Reverse proxy**: Caddy (auto SSL) or nginx - **Process**: Docker Compose - **Database**: Docker Postgres with named volume, or managed Postgres (Neon, Supabase, Railway) ## Get the code onto the server Clone the repo on your server the same way you did locally, then create a production `.env` (not `.env.local` — that's the dev-only file): ```bash git clone https://github.com/answerLoops/answerLoops.git cd answerLoops cp .env.local.example .env ``` Fill in `.env` with your production values — the checklist below covers what has to be different from your dev setup. ## Environment checklist Before going live, verify these are set in your production `.env`: - [ ] `AUTH_URL` set to your production domain (e.g. `https://app.yourdomain.com`) - [ ] `AUTH_SECRET` — unique 32-byte hex, not the same as dev - [ ] `ENCRYPTION_KEY` — unique 32-byte hex, not the same as dev - [ ] `DATABASE_URL` pointing to production database - [ ] OAuth callback URLs updated in GitHub/Google dashboards - [ ] `OPENAI_API_KEY` set ## Caddy (recommended — auto SSL) ``` app.yourdomain.com { reverse_proxy localhost:3000 } ``` Run: `caddy run --config Caddyfile` ## nginx ```nginx server { listen 443 ssl; server_name app.yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## Start in production Use `docker-compose.prod.yml`, not the plain `docker-compose.yml` from local dev — it builds the multi-stage production image and runs the bot in `bot:start` mode instead of the dev watch mode: ```bash docker compose -f docker-compose.prod.yml up -d --build ``` Drizzle migrations run automatically against your production database on first start, same as local dev — no separate migration step. The **bot** service must be running for knowledge-base syncs (Notion, GitHub) to complete — it's the worker that picks up queued sync jobs and drives them. It reaches the app at `BOT_TARGET_URL` (default `http://localhost:3000`) and authenticates with `BOT_SECRET`; set both if the two services aren't on the same host. The sync itself runs inside `POST /api/kb/sync-jobs/run` on the app, which does real work after responding to auth — deploy the app on a runtime without a short per-request timeout (the standard `pnpm start` Node server is fine; a serverless function platform may cut a large sync off). ## Health check ```bash curl https://app.yourdomain.com/api/health # → {"ok":true} ``` ## Alternative: Railway or another PaaS If you'd rather not manage a VPS, reverse proxy, and TLS certs yourself, push the repo to Railway (or another PaaS) — Railway reads the committed `railway.toml` / `railway.bot.toml`, builds each service with Nixpacks, handles HTTPS automatically, and you skip the Caddy/nginx section above entirely. You'll still need to set the same environment variables, and a managed Postgres (Neon, Railway's own Postgres) instead of the Docker-volume one. --- # Doc: self-hosting/security-scanning URL: https://answerloops.com/docs/self-hosting/security-scanning --- title: Security Scanning description: What answerLoops does to ship secure releases, and what you should check when self-hosting. --- Every answerLoops release goes through automated dependency, secret, and code-level security scanning before it merges — you're deploying code that's already been checked. This page covers what that means for you as a self-hoster, and the security practices worth following in your own deployment. ## What's already covered before you deploy Every change to the codebase is scanned for known-vulnerable dependencies, leaked credentials, and common code-level security issues (injection, insecure crypto, missing auth checks) before it's merged. You don't need to run anything yourself to benefit from this — it's already reflected in the code you `git clone` or `docker pull`. The scanners themselves — Trivy (dependencies, secrets, image), Semgrep (SAST), and Zizmor (GitHub Actions workflow audit) — run at pinned versions, and Dependabot opens a PR whenever a newer version is released, so the tooling stays current without silently changing under CI. ## What you're responsible for in your own deployment Security fixes ship as regular releases. See [Upgrading](/docs/self-hosting/upgrading) — pull the latest image or `git pull` + rebuild regularly rather than pinning to an old version indefinitely. AI provider API keys and bot tokens are encrypted at rest using AES-256-GCM, keyed by `ENCRYPTION_KEY`. A production build (`NODE_ENV=production`) refuses to save a credential without it, rather than falling back to plaintext — see [Environment Variables](/docs/self-hosting/environment-variables). Generate one with `openssl rand -hex 32` and never commit it to source control. Auth.js signs and encrypts session tokens with `AUTH_SECRET`. Generate a unique value per deployment — never reuse the example value from `.env.local.example`. Terminate TLS in front of answerLoops (Railway, Fly.io, and most PaaS providers do this automatically; if you're running raw Docker on your own box, put a reverse proxy like Caddy or nginx in front of it). OAuth callbacks and webhook signature checks assume the deployment is reachable over HTTPS. `GITHUB_WEBHOOK_SECRET`, `SLACK_SIGNING_SECRET`, `RESEND_WEBHOOK_SECRET`, and `BOT_SECRET` all guard inbound webhook endpoints against forged requests. Treat them the same as API keys — never commit them, never log them. Postgres should not be reachable from the public internet. If you're using Neon, Railway, or a managed provider, this is the default — if you're self-managing Postgres, firewall it to only the app and bot services. ## Reporting a vulnerability If you find a security issue in answerLoops itself, open an issue on [GitHub](https://github.com/answerLoops/answerLoops/issues) or reach out through the contact info in the repo. Please don't include working exploit details in a public issue for anything that looks exploitable in a live deployment — flag it first so it can be triaged privately. --- # Doc: self-hosting/slack-app URL: https://answerloops.com/docs/self-hosting/slack-app --- title: Slack App Setup description: Set up Slack for answerLoops — 1-click OAuth or polling mode (no webhook, no admin required). --- answerLoops offers two Slack connection paths. Both feed into the same AI pipeline. Pick based on your security posture. ## Option A: 1-click OAuth (recommended) ### 1. Create a Slack App 1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch** 2. Name it (e.g. "answerLoops") and select your workspace ### 2. Configure OAuth scopes Go to **OAuth & Permissions** → **Bot Token Scopes** and add: - `channels:history` - `channels:read` - `channels:join` - `chat:write` - `reactions:write` - `users:read` `channels:join` lets answerLoops add the bot to a public channel automatically the moment you select it in the channel picker — without it, Slack never adds the bot to a channel on its own, so `conversations.history` fails with `not_in_channel` and messages silently never ingest. Private channels are a hard Slack platform limit either way: a bot can never join one on its own, so those still need a human to add the app via the channel's **Integrations → Add apps**. `users:read` resolves a message's raw user ID to a display name — without it, tickets show an opaque ID (e.g. `U0BMB9H6SFQ`) instead of a name. **Already connected before August 2026?** Slack scopes aren't retroactive — your existing bot token keeps its old scopes until you reauthorize. Go to **Settings → Integrations → Slack**, click **Disconnect**, then **Add to Slack** again to pick up `users:read` and start showing real names. Until then, tickets keep showing the raw user ID exactly as they do today — nothing breaks either way. ### 3. Add redirect URL Go to **OAuth & Permissions** → **Redirect URLs** and add: ``` https://your-domain.com/api/slack/callback ``` Replace `your-domain.com` with your `AUTH_URL` value. ### 4. Get credentials Go to **Basic Information** → **App Credentials** and copy: ```bash SLACK_CLIENT_ID=. SLACK_CLIENT_SECRET= SLACK_SIGNING_SECRET= AUTH_URL=https://your-domain.com ``` ### 5. Connect in answerLoops Go to **Settings → Integrations → Slack** → **Add to Slack**. Authorize the workspace. The channel picker loads automatically. ### 6. (Optional) Enable real-time Events API By default answerLoops polls for new messages. For instant delivery: 1. **Event Subscriptions** → Enable Events → Request URL: `https://your-domain.com/api/slack/events` 2. Subscribe to bot events: `message.channels`, `reaction_added` Events API requires a public HTTPS URL. If your deployment is behind a firewall or VPN, use polling mode — same pipeline, no inbound connections needed. --- ## Option B: Polling mode (no admin, no webhook) Designed for teams that cannot expose a public webhook URL, cannot get security approval for inbound HTTP from Slack, or simply prefer outbound-only connections. **What you don't need:** - No public-facing URL - No inbound firewall rules - No admin approval - No signing secret The bot reaches out to Slack's API on a schedule — Slack never calls back. ### 1. Create a Slack App (minimal scopes) 1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch** 2. Name it and select your workspace 3. **OAuth & Permissions** → **Bot Token Scopes** — add only: - `channels:history` - `channels:read` - `channels:join` - `chat:write` - `reactions:write` - `users:read` ### 2. Install and copy token Click **Install to Workspace** → **Allow**. Copy the **Bot User OAuth Token** (`xoxb-...`). ### 3. Get Team ID and Channel IDs - **Team ID**: your workspace URL — `https://app.slack.com/client/T0123ABCDE` — the `T…` part - **Channel IDs**: right-click a channel → **Copy link** — last segment (e.g. `C01234ABCDE`) ### 4. Connect in answerLoops Go to **Settings → Integrations → Slack** → click **Set up manually instead**. Enter the bot token, team ID, and channel IDs. ### 5. (Optional) Adjust poll interval ```bash SLACK_POLL_INTERVAL_SECONDS=60 # default. Minimum recommended: 30. ``` Polling uses `conversations.history` with cursor-based deduplication — messages are never processed twice even if polls overlap. --- ## Invite the bot to channels For **public** channels, selecting them in **Settings → Integrations → Slack**'s channel picker joins the bot automatically — no manual step needed, as long as the `channels:join` scope above is granted. **Private** channels can't be auto-joined by any bot — that's a Slack platform limit, not an answerLoops one. Add the app manually: open the channel → **Integrations** tab → **Add apps** → select your Slack app (more reliable than `/invite @YourBotName`, which doesn't always work for bot users depending on workspace settings). --- # Doc: self-hosting/telegram-bot URL: https://answerloops.com/docs/self-hosting/telegram-bot --- title: Telegram Bot Setup description: Connect answerLoops to a Telegram group or supergroup. --- ## How it works answerLoops uses Telegram's **webhook API** — no persistent process required. Telegram pushes updates to your answerLoops instance via `POST /api/telegram/webhook`. The platform triage, embeds, and AI answer the question, then replies in the same chat. ## 1. Create a bot with BotFather 1. Open Telegram and search for **@BotFather** 2. Send `/newbot` and follow the prompts 3. Copy the bot token — format: `123456789:AAHdqTcv...` ## 2. Configure in answerLoops Settings Go to **Integrations → Telegram** and enter: - **Bot Token** — from BotFather - **Chat IDs** (optional) — group/supergroup IDs to monitor. Leave blank to monitor all chats the bot is added to. Group chat IDs are negative numbers (e.g. `-1001234567890`). Forward a message to [@userinfobot](https://t.me/userinfobot) to get the chat ID. - **Escalation username** (optional) — @mentioned when AI confidence is below threshold - **Confidence threshold** — 0–1, default 0.8 Click **Connect** to save. ## 3. Add the bot to your group In Telegram, add your bot to the group or supergroup you want to monitor. The bot needs permission to read messages. For a supergroup: go to Group Info → Edit → Administrators → Add Administrator → search your bot name. ## 4. Register the webhook After saving the token, click **Register webhook** in Settings. This calls Telegram's `setWebhook` API and points it at your answerLoops instance. You can also call this endpoint directly: ```bash POST /api/telegram/register ``` Requires an active session (dashboard login). ## 5. Verify Send a message in your monitored group. A new ticket should appear in `/tickets` within a few seconds. ## Environment variables No additional env vars are required beyond the token saved in Settings. For a platform default (before any org configures their own token): ```dotenv TELEGRAM_BOT_TOKEN=123456789:AAHdqTcv... ``` ## Notes - Telegram group chats IDs are negative integers. Supergroup IDs start with `-100`. - The bot only processes text messages. Stickers, photos, and media are silently ignored. - Messages under 10 characters are ignored (too short to be a support question). - The webhook secret is auto-generated per org and verified on every incoming update — Telegram cannot spoof messages. - Unlike Discord (persistent gateway), Telegram is fully stateless — webhook delivery is Telegram's responsibility. --- # Doc: self-hosting/troubleshooting URL: https://answerloops.com/docs/self-hosting/troubleshooting --- title: Troubleshooting description: Fixes for common issues encountered when self-hosting answerLoops. --- ## App won't start **Check logs first:** ```bash docker compose logs app --tail=50 ``` **Common causes:** - **Missing required env var** — the logs will name the variable. Add it to `.env` and restart. - **`DATABASE_URL` wrong or Postgres not ready** — Postgres takes a few seconds to initialise on first boot. Wait 10 seconds and run `docker compose restart app`. - **Port 3000 already in use** — change the host port in `docker-compose.yml`, e.g. `"3001:3000"`. - **`AUTH_SECRET` or `AUTH_URL` missing** — answerLoops uses Auth.js v5. The correct variable names are `AUTH_SECRET` and `AUTH_URL`, **not** `NEXTAUTH_SECRET` / `NEXTAUTH_URL`. Rename them if you are upgrading from an older deployment. ## Database connection refused ``` Error: connect ECONNREFUSED 127.0.0.1:5432 ``` Postgres is not yet ready or is on a different host. Fix: ```bash docker compose restart app ``` If the error persists, verify that the `postgres` service is running and healthy: ```bash docker compose ps docker compose logs postgres --tail=20 ``` Check that `DATABASE_URL` in your `.env` uses the service name as the host (`postgres`), not `localhost`: ``` DATABASE_URL=postgresql://community:password@postgres:5432/community ``` ## OAuth redirect mismatch **Error:** `redirect_uri_mismatch` or `Error 400: redirect_uri_mismatch` Add the exact callback URL to your OAuth app's allowed redirect URIs: | Provider | Callback URL | |---|---| | GitHub | `https:///api/auth/callback/github` | | Discord | `https:///api/auth/callback/discord` | | Google | `https:///api/auth/callback/google` | Also verify that `AUTH_URL` in your `.env` matches your actual public domain exactly, including the scheme (`https://`), and has no trailing slash. ## AI draft never appears No AI provider is configured. The draft panel is hidden when there is no valid AI key. Fix: go to **Settings → AI Model** and add an API key, or set one of the platform-default env vars (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `GROQ_API_KEY`, or `MISTRAL_API_KEY`) and restart the app. Verify the key is accepted — the Settings page shows a "Key saved ✓" badge after a successful save. ## GitHub webhook returning 401 Unauthorized The webhook receives a 401 if either of the following is true: 1. **`/api/github/webhook` is not in `PUBLIC_PATHS`** — Auth.js v5 protects all routes by default. The webhook route must be explicitly excluded in `auth.ts`. Check that `PUBLIC_PATHS` includes `/api/github/webhook`. 2. **`GITHUB_WEBHOOK_SECRET` has a trailing newline** — `openssl rand -hex 32` appends a newline character. If you paste the output directly, the secret stored in your `.env` will not match what GitHub sends. Fix: - Regenerate with `openssl rand -hex 32 | tr -d '\n'`, or - Open `.env`, find `GITHUB_WEBHOOK_SECRET`, and manually remove any trailing whitespace After fixing, restart the app and re-deliver the failed webhook from the GitHub App settings page. ## Discord forum posts not creating tickets **Symptom:** questions posted as new forum posts are not ingested; replies inside existing threads work fine. **Cause:** Discord fires a `ThreadCreate` event for new forum posts, not a `MessageCreate` event. The bot must have a `ThreadCreate` listener to capture these. **Fix:** ensure you are running the bot image built after the July 2026 `ThreadCreate` fix. Pull the latest image and restart: ```bash docker compose pull bot docker compose up -d bot ``` Replies inside threads fire `MessageCreate` and continue to work as before. Only the initial forum post requires `ThreadCreate`. ## Discord channel picker shows "loading…" indefinitely The channel picker in the answerLoops UI calls Discord's API to list channels. It requires `DISCORD_TOKEN` to be set on the **app** service, not only on the bot service. Check your `docker-compose.yml` or environment config and ensure `DISCORD_TOKEN` is present in the `app` service's environment block, then restart: ```bash docker compose up -d app ``` ## Discord bot not connecting (`DISCORD_TOKEN` and `DISCORD_CLIENT_ID` mismatch) `DISCORD_TOKEN` and `DISCORD_CLIENT_ID` must both come from the **same Discord application**. If you created a new application or regenerated the token, update both variables together and restart the bot. ## `BOT_TARGET_URL` causing 404 on `/api/ingest` The bot appends `/api/ingest` to `BOT_TARGET_URL`. A trailing slash or period produces a malformed URL: | Value | Result | |---|---| | `https://answerloops.com` | ✅ `https://answerloops.com/api/ingest` | | `https://answerloops.com/` | ❌ `https://answerloops.com//api/ingest` | | `https://answerloops.com.` | ❌ `https://answerloops.com./api/ingest` | Set `BOT_TARGET_URL` to the bare domain with no trailing slash or period. ## FAQ page always shows "No FAQ generated yet" **Cause:** a missing `await` on the `getLatestFAQ()` call in the GET `/api/faq` route caused the response to resolve before the DB query completed. This was fixed in July 2026. If you are on an older build, pull the latest image: ```bash docker compose pull app docker compose up -d app ``` Note: the FAQ is generated only from tickets with status `resolved` or `closed`. If no tickets have been resolved yet, the FAQ will legitimately be empty. ## `/knowledge-gaps` page crashes with a server error **Cause:** a nested `` inside another `` caused an SSR crash when gap data existed. Fixed in July 2026. Pull the latest app image to resolve. ## Slack events not ingesting 1. Verify `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, and `SLACK_SIGNING_SECRET` are all set correctly. 2. Check the Slack App's Event Subscriptions page — the request URL must be publicly reachable and returning 200. 3. Confirm `SLACK_POLL_INTERVAL_SECONDS` is set if you are using polling mode rather than event subscriptions. 4. Check app logs for Slack-related errors: ```bash docker compose logs app --tail=50 | grep -i slack ``` ## Emails not sending 1. Verify `RESEND_API_KEY` is valid and not expired. 2. Verify `RESEND_FROM` is set to an address in a verified Resend domain. 3. Check that the sending domain is verified in your Resend dashboard. 4. Inspect logs: ```bash docker compose logs app --tail=50 | grep -i resend ``` ## Widget returns 500 A 500 from the widget embed script usually means the app is not reachable from the browser, or the widget's org token is invalid. 1. Open the browser console — the error message from the fetch response is logged there. 2. Confirm the app is running and accessible at its public URL. 3. Go to **Settings → Widget** and verify the org token has not been regenerated without updating the embed snippet. ## Data lost after restart You ran `docker compose down -v`. The `-v` flag deletes all named volumes, including `postgres-data`. This is permanent — there is no recovery without a backup. Always stop the stack with: ```bash docker compose down ``` Never use `-v` unless you intentionally want to wipe all data. ## Page crashes with "X.map is not a function" An API route returned a non-array (usually an error object or `null`). Check: ```bash docker compose logs app --tail=30 | grep -i error ``` Common causes: missing API key, failed DB query, or an unhandled exception in an API handler. ## Onboarding wizard shows on every login `onboarded_at` is null in the `orgs` table — the onboarding wizard was never completed. Complete it once: name your workspace, then click through all steps (you can skip optional ones). To inspect via the database directly: ```bash docker compose exec postgres psql -U community -d community \ -c "SELECT id, name, onboarded_at FROM orgs;" ``` ## Updated content appears without its styles Let Next.js manage caching for `/_next/static/` assets. Production builds use content-hashed assets that can be cached indefinitely; development assets need to refresh as source files change. Do not apply a custom immutable cache rule to development assets. If an earlier configuration cached a development stylesheet indefinitely, restart the development server after correcting the configuration and perform a hard reload in the browser to discard the old response. --- # Doc: self-hosting/upgrading URL: https://answerloops.com/docs/self-hosting/upgrading --- title: Upgrading description: How to update your self-hosted answerLoops instance. --- ## Standard upgrade ```bash git pull origin main docker compose up --build -d ``` Drizzle migrations run automatically on startup. ## Check migration status ```bash docker compose exec postgres psql -U community -d community \ -c "SELECT * FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 5;" ``` ## Rollback answerLoops doesn't ship automatic rollback tooling. To revert: ```bash git checkout docker compose up --build -d ``` If a migration added new columns or tables, rolling back the code won't remove them. This is generally safe — extra columns are ignored. Dropped columns may cause issues; check the migration diff before rolling back. ## Check current version ```bash git log --oneline -1 ```