# Troubleshoot the Achla AI widget

> Why the Achla AI search widget does not appear or does not answer: a decision tree, every console message and API error code (AISX-…), and how to fix each.

HTML: https://achlaai.com/en/docs/troubleshooting · Markdown: https://achlaai.com/docs/troubleshooting.md

<a id="triage"></a>
## Quick triage: the widget does not appear

Work through these steps in order — each one rules out a whole class of causes.

1. **Is the tag in the page HTML?** Run the command below; no match means the tag is not installed on this page or an old cached copy of the page is served — add the tag and purge the page cache. (CMS plugins use their own loader, so `/w.js` does not appear there — check the plugin's status page instead.)
2. **Is the key enabled?** Run the `widget-config` command below. `{"enabled":false}` → the key is unknown or replaced, the project is not active, indexing is not finished, or ownership/plugin connection is invalid. First compare the key in the tag with **Settings → Embed code**: a stale or deleted key is not flagged in the dashboard. If they differ, replace the whole tag with the current one. If they match, the dashboard (**Overview**) shows project, indexing and connection status — see [AISX-W110](#AISX-W110). Do not change CSP or caching for this.
3. **Enabled, but nothing visible?** If the command returns `"enabled":true`, open the page with DevTools → Console and look for lines starting with `[ai-search]`. Each carries an `AISX-…` code — find it in [Console messages](#console-messages).
4. **Console is silent?** See [Silent failures](#silent-failures): a CSP that blocks the script, a plugin that did not output the loader, a CMS plugin in Inline mode on a page without its shortcode/block (expected — no floating button there), or page CSS covering the widget.

1 — expect exactly one `<script …/w.js …>` tag.

```bash
curl -s "https://YOUR-SITE.example" | grep -o '<script[^>]*/w\.js[^>]*>'
```

2 — expect `"enabled":true`.

```bash
curl -s "https://achlaai.com/api/widget-config?key=YOUR_PUBLIC_KEY"
```

> **Inline mode and the floating button.** If you expected an inline search box but see the floating button, the page has no `<div id="ai-search"></div>` — [AISX-W104](#AISX-W104). If you see both, the widget is included twice — [AISX-W105](#AISX-W105).

<a id="silent-failures"></a>
## Silent failures (no [ai-search] line in the console)

| Situation | How to recognise it | Fix |
| --- | --- | --- |
| The script is blocked by the site's CSP | The console shows a Content-Security-Policy violation for `w.js` (browser message, not `[ai-search]`); the Network tab shows the request as blocked. | Allow `https://achlaai.com` in `script-src` and `connect-src` — see [the install guide](https://achlaai.com/en/docs/install#csp). |
| The tag is missing on this page or the HTML is cached | The `grep` command in [Quick triage](#triage) finds nothing. | Add the tag (`<script src="https://achlaai.com/w.js" data-key="YOUR_PUBLIC_KEY" defer></script>`) to the page template and purge the page/CDN cache. |
| CMS plugin with Inline placement: no floating button on other pages | Pages without the WordPress shortcode `[achla_search]` / “Achla AI Search” block, or without the Drupal “Achla AI Search” block, show no widget at all. This is expected: the plugins use one placement mode for the whole site and, in Inline, load the widget only where the shortcode/block is rendered. | Insert the shortcode or block on that page, or choose Bubble placement in the plugin settings. For a floating button everywhere plus an inline box on one page, use manual tags — see [Mixed placement](https://achlaai.com/en/docs/install#mixed-placement). |
| A CMS plugin did not output the widget | No widget, no console line. The plugins record placement/release problems as `data-achla…` attributes on `<html>`: run `document.documentElement.getAttributeNames().filter(n => n.startsWith('data-achla'))` in the console. | Open the plugin settings: check the connection, indexing status, the “show the widget” switch and the placement diagnostics; reconnect if the connection was revoked. |
| The widget is covered by page elements | `document.querySelectorAll('.aisx-btn, .aisx-inline, .aisx-drop').length` is above 0, but nothing is visible. | The floating button and panel use `z-index: 2147483000`; check for theme elements with a higher z-index or for CSS that hides the `#ai-search` container. |

Older widget builds printed the same messages without the `AISX-…` code (for example `[ai-search] inline: no <div id="ai-search"> on this page, falling back to bubble`). Match them by text against the signals below.

<a id="changes-take-effect"></a>
## When do changes take effect?

- **Dashboard settings** (placement mode, attach selector, appearance) reach visitors on the next page load within about 30 seconds. `/api/widget-config` is served with `Cache-Control: no-store`; each server instance caches widget settings for 30 seconds and the project record for 5 seconds. The tag on your page does not change, so no page or CDN cache purge is needed.
- **The widget code** (`/w.js`) is served with `Cache-Control: public, max-age=3600, must-revalidate`, so a new widget version can take up to an hour to reach a visitor's browser.
- **Changes to the tag itself** (adding it, `data-mode`, `data-attach`, `data-position`, the key) are changes to your page HTML and take effect when your site serves the new HTML.

<a id="console-messages"></a>
## Console messages (AISX-W1xx)

Every message the widget prints has the form `[ai-search] <CODE> <message> — <link>`, where the link points to the matching section below.

| Code | Signal | Meaning |
| --- | --- | --- |
| [AISX-W101](#AISX-W101) | `AISX-W101 missing data-key` | The script tag has no data-key |
| [AISX-W102](#AISX-W102) | `AISX-W102 attach: invalid selector, falling back to bubble` | Attach selector is not valid CSS |
| [AISX-W103](#AISX-W103) | `AISX-W103 attach: no input matched the selector, falling back to bubble` | Attach selector matched nothing on this page |
| [AISX-W104](#AISX-W104) | `AISX-W104 inline: no <div id="ai-search"> on this page, falling back to bubble` | Inline mode, but the page has no #ai-search container |
| [AISX-W105](#AISX-W105) | `AISX-W105 inline: target is already used, falling back to bubble` | The #ai-search container is already used by another copy of the widget |
| [AISX-W106](#AISX-W106) | `AISX-W106 attach: matched element is not a free text input, falling back to bubble` | Attach selector matched an element that cannot be used |
| [AISX-W110](#AISX-W110) | `AISX-W110 widget is disabled for this key (unknown key, index not ready, or access revoked) — check your dashboard` | The widget is disabled for this key |
| [AISX-W111](#AISX-W111) | `AISX-W111 could not load widget configuration (network, CSP or server error)` | The widget configuration could not be loaded |
| [AISX-W112](#AISX-W112) | `AISX-W112 data-key has an invalid format` | The data-key value is malformed |
| [AISX-W120](#AISX-W120) | `AISX-W120 search request failed:` | A search request returned an error |
| [AISX-W121](#AISX-W121) | `AISX-W121 search request did not complete (network error or timeout)` | A search request did not complete |

<a id="AISX-W101"></a>
### AISX-W101 — The script tag has no data-key

Printed with `console.error`.

```text
[ai-search] AISX-W101 missing data-key — <link to this section>
```

**Cause.** The `<script>` that loads the widget has no `data-key` attribute (or it is empty). The widget cannot tell which site it belongs to, stops immediately and renders nothing.

**Fix.** Copy the embed code again from your dashboard (**Settings → Embed code**): it already contains `data-key="pk_…"`. If you add the tag with JavaScript, set `data-key` on the element before you append it to the page.

<a id="AISX-W102"></a>
### AISX-W102 — Attach selector is not valid CSS

Printed with `console.warn`.

```text
[ai-search] AISX-W102 attach: invalid selector, falling back to bubble — <link to this section>
```

**Cause.** Placement mode is `attach`, but the selector (from `data-attach` or from the dashboard) is not a valid CSS selector — `document.querySelector()` threw an error. The widget shows the floating button (bubble) instead.

**Fix.** Use a valid CSS selector for your site's search field, for example `input[type="search"]` or `#search-input`. Set it in the dashboard (**Settings → Widget mode**) or in `data-attach` in the tag, or switch placement back to bubble. Dashboard changes reach visitors on the next page load within about 30 seconds — no cache purge needed ([When do changes take effect?](#changes-take-effect)). After saving, reload the page and run `document.querySelector('<your selector>')` in the browser console — it must return the search `<input>` (type `text` or `search`), and the console must no longer show AISX-W102.

<a id="AISX-W103"></a>
### AISX-W103 — Attach selector matched nothing on this page

Printed with `console.warn`.

```text
[ai-search] AISX-W103 attach: no input matched the selector, falling back to bubble — <link to this section>
```

**Cause.** Placement mode is `attach`, but no selector is configured, or the selector matches no element on this page at the moment the widget mounts. The widget shows the floating button (bubble) instead.

**Fix.** 1. Test the selector before saving it: on the live page, run `document.querySelector('<selector>')` in the browser console — it must return the search `<input>` (type `text` or `search`) that exists when the page loads. Adjust the selector until it does. 2. Save it in the dashboard (**Settings → Widget mode**) or in `data-attach` on the tag — or switch to bubble. If the field exists only on some pages, the bubble on the other pages is expected. 3. Reload the page and run the same `document.querySelector('<selector>')` again: it must still return the input, and the console must no longer show AISX-W103. Dashboard changes reach visitors on the next page load within about 30 seconds — no cache purge needed ([When do changes take effect?](#changes-take-effect)).

<a id="AISX-W104"></a>
### AISX-W104 — Inline mode, but the page has no #ai-search container

Printed with `console.warn`.

```text
[ai-search] AISX-W104 inline: no <div id="ai-search"> on this page, falling back to bubble — <link to this section>
```

**Cause.** Placement mode is `inline` (set in the dashboard or with `data-mode="inline"`), but no element with `id="ai-search"` exists when the widget mounts. The widget shows the floating button (bubble) instead of the inline search box.

**Fix.** Add `<div id="ai-search"></div>` to the page HTML exactly where the search box should appear (WordPress plugin: the `[achla_search]` shortcode or the “Achla AI Search” block; Drupal module: place the “Achla AI Search” block). Or switch placement back to bubble in the dashboard. A container inserted later by client-side JavaScript is not picked up. Dashboard changes reach visitors on the next page load within about 30 seconds — no cache purge needed ([When do changes take effect?](#changes-take-effect)). After the change, reload the page: the console must no longer show AISX-W104.

<a id="AISX-W105"></a>
### AISX-W105 — The #ai-search container is already used by another copy of the widget

Printed with `console.warn`.

```text
[ai-search] AISX-W105 inline: target is already used, falling back to bubble — <link to this section>
```

**Cause.** The `#ai-search` element was already claimed by another instance of the widget — almost always because the widget script is included twice on the page. The second copy falls back to the floating button, so the page shows both.

**Fix.** Keep exactly one widget tag per page. In the browser console, `document.querySelectorAll('script[src$="/w.js"], script[src*="/widget/releases/"]').length` must be `1`. Remove the extra copy (theme template, tag manager, or a plugin plus a manual tag).

<a id="AISX-W106"></a>
### AISX-W106 — Attach selector matched an element that cannot be used

Printed with `console.warn`.

```text
[ai-search] AISX-W106 attach: matched element is not a free text input, falling back to bubble — <link to this section>
```

**Cause.** Placement mode is `attach` and the selector matched an element, but it is not an `<input>` of type `text` or `search` (for example a `<form>`, a wrapper `<div>`, a button or a `<textarea>`), or another copy of the widget has already attached to it. The widget shows the floating button (bubble) instead. Only the first element matched by the selector is checked.

**Fix.** Point the selector at the search `<input>` itself, e.g. `form.search input[type="search"]` instead of `form.search`. If the field is already taken, remove the duplicate widget tag (see [AISX-W105](#AISX-W105)). Dashboard changes reach visitors on the next page load within about 30 seconds — no cache purge needed ([When do changes take effect?](#changes-take-effect)). After saving, reload the page and run `document.querySelector('<your selector>')` in the browser console — it must return the search `<input>` (type `text` or `search`), and the console must no longer show AISX-W106.

<a id="AISX-W110"></a>
### AISX-W110 — The widget is disabled for this key

Printed with `console.warn`.

```text
[ai-search] AISX-W110 widget is disabled for this key (unknown key, index not ready, or access revoked) — check your dashboard — <link to this section>
```

**Cause.** `/api/widget-config` answered `{"enabled":false}`. Possible reasons: the key is unknown or was deleted/replaced, the project is not active, the search index is not ready yet, or site ownership / the CMS plugin connection is no longer valid. The public endpoint deliberately does not say which. The widget renders nothing.

**Fix.** 1. Compare the `data-key` in the page's tag with the key in **Settings → Embed code**. If they differ (the old key was deleted or replaced), replace the whole tag with the current one — a stale or deleted key is not flagged in the dashboard, where the project can look healthy. 2. If the keys match, open **Overview**: project status, index readiness and site connection are shown there. If you connected through the WordPress or Drupal plugin, check the connection status in the plugin settings and reconnect if needed. Do not change CSP or caching — the request itself succeeded. Re-check with the `widget-config` check in [Quick triage](#triage): it must return `"enabled":true`.

<a id="AISX-W111"></a>
### AISX-W111 — The widget configuration could not be loaded

Printed with `console.warn`.

```text
[ai-search] AISX-W111 could not load widget configuration (network, CSP or server error) — <link to this section>
```

**Cause.** Three attempts to `GET /api/widget-config` failed: network error or timeout (1.5 s per attempt), the request was blocked by the site's Content-Security-Policy (`connect-src`), the server kept answering HTTP 429/5xx, or the response was not valid JSON. Without a configuration the widget renders nothing (it never shows itself without an explicit `"enabled":true`).

**Fix.** Open DevTools → Network and find the `widget-config` request. Blocked by CSP → allow the widget origin in `connect-src` (see the install guide, “Content-Security-Policy”). 5xx/timeouts → retry in a few minutes. Check the endpoint directly with the `widget-config` check in [Quick triage](#triage).

<a id="AISX-W112"></a>
### AISX-W112 — The data-key value is malformed

Printed with `console.error`.

```text
[ai-search] AISX-W112 data-key has an invalid format — <link to this section>
```

**Cause.** `/api/widget-config` answered HTTP 400: the `data-key` value is not a plausible public key (allowed: letters, digits, `_`, `.`, `-`, at most 128 characters). Typical causes: a typo, a truncated key, extra spaces or quotes, or a placeholder such as `pk_XXXX` left in the snippet. The widget renders nothing.

**Fix.** Copy the exact key from **Settings → Embed code** in your dashboard (it starts with `pk_`) and replace the whole `data-key` value. Do not edit the key by hand.

<a id="AISX-W120"></a>
### AISX-W120 — A search request returned an error

Printed with `console.warn`.

```text
[ai-search] AISX-W120 search request failed: <error code> — <link to this section>
```

**Cause.** The visitor asked a question and `/api/search` answered with an error. The console line continues with the server's error code, e.g. `AISX-W120 search request failed: quota_exceeded`; it ends with `unknown` when the response carried no recognisable code (for example an HTML error page from a proxy or CDN instead of JSON). The visitor sees a short message in the widget.

**Fix.** Look up the trailing code in the [search API error table](#api-errors) (AISX-A201…AISX-A214) — each entry explains the cause and the fix.

<a id="AISX-W121"></a>
### AISX-W121 — A search request did not complete

Printed with `console.warn`.

```text
[ai-search] AISX-W121 search request did not complete (network error or timeout) — <link to this section>
```

**Cause.** The request to `/api/search` failed before any response arrived (network error, request blocked by the site's CSP `connect-src`, or no answer within 60 seconds). The visitor sees an error message in the widget.

**Fix.** Open DevTools → Network and repeat the question. Blocked by CSP → allow the widget origin in `connect-src`. Timeouts → retry; if they persist, contact support from your dashboard with the time of the request.

<a id="api-errors"></a>
## Search API errors (AISX-A2xx)

When a visitor's question fails, `/api/search` answers with a JSON body containing `error` (and a visitor-facing `message`). Find it in DevTools → Network → the `search` request → Response, or at the end of the console line [AISX-W120](#AISX-W120).

| Code | Signal | Meaning |
| --- | --- | --- |
| [AISX-A201](#AISX-A201) | `missing_key_or_query` | Key or question is missing |
| [AISX-A202](#AISX-A202) | `bad_request` | Malformed request |
| [AISX-A203](#AISX-A203) | `payload_too_large` | Request body too large |
| [AISX-A204](#AISX-A204) | `query_too_long` | Question too long |
| [AISX-A205](#AISX-A205) | `invalid_key` | Unknown or inactive key |
| [AISX-A206](#AISX-A206) | `origin_not_allowed` | The widget is used on a site the key is not registered for |
| [AISX-A207](#AISX-A207) | `project_inactive` | Project is not active |
| [AISX-A208](#AISX-A208) | `not_ready` | Search index is not ready |
| [AISX-A209](#AISX-A209) | `project_payment_overdue` | Subscription payment is overdue |
| [AISX-A210](#AISX-A210) | `rate_limited` | Too many requests |
| [AISX-A211](#AISX-A211) | `daily_cap_exceeded` | Daily safety cap reached |
| [AISX-A212](#AISX-A212) | `quota_exceeded` | Search quota used up |
| [AISX-A213](#AISX-A213) | `quota_unavailable` | Quota check temporarily unavailable |
| [AISX-A214](#AISX-A214) | `answer_failed` | The answer could not be generated |

<a id="AISX-A201"></a>
### AISX-A201 — Key or question is missing

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 400 | `missing_key_or_query` | That question couldn't be sent. Please try again. |

**Cause.** The JSON body has an empty or missing `key` or `query`. The widget always sends both, so this normally comes from a hand-written API call.

**Fix.** Send `{"key":"pk_…","query":"…"}` with `Content-Type: application/json`.

<a id="AISX-A202"></a>
### AISX-A202 — Malformed request

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 400 | `bad_request` | That question couldn't be sent. Please try again. |

**Cause.** The request body is not valid JSON, or the question is empty after control characters and extra whitespace are removed.

**Fix.** Send a valid JSON body with a non-empty `query`. With the stock widget this is rare — retry the question.

<a id="AISX-A203"></a>
### AISX-A203 — Request body too large

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 413 | `payload_too_large` | Your question is too long. Please shorten it to 500 characters or fewer. |

**Cause.** The request body exceeds 16 KB, or the question exceeds 4096 characters.

**Fix.** Shorten the question (questions longer than 500 characters are rejected anyway, see [AISX-A204](#AISX-A204)).

<a id="AISX-A204"></a>
### AISX-A204 — Question too long

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 400 | `query_too_long` | Your question is too long. Please shorten it to 500 characters or fewer. |

**Cause.** After whitespace is collapsed, the question is longer than 500 characters.

**Fix.** Ask a shorter question (at most 500 characters).

<a id="AISX-A205"></a>
### AISX-A205 — Unknown or inactive key

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 401 | `invalid_key` | Search isn't available on this site. |

**Cause.** The key has an invalid format, does not exist, or has been deactivated (for example replaced by a new key).

**Fix.** Compare `data-key` on the page with **Settings → Embed code** in your dashboard; if they differ (the old key was deleted or replaced), replace the whole tag with the current one. A stale or deleted key is not flagged in the dashboard — the project can look healthy there. The same key makes `/api/widget-config` answer `{"enabled":false}` ([AISX-W110](#AISX-W110)).

<a id="AISX-A206"></a>
### AISX-A206 — The widget is used on a site the key is not registered for

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 403 | `origin_not_allowed` | Search isn't available on this site. |

**Cause.** The key is bound to the site registered for its project, and the request's `Origin` is not one of its origins. Allowed: the registered host over `http` or `https`, plus its `www.` / bare-domain counterpart. Other domains, staging subdomains and non-default ports are rejected.

**Fix.** Use the widget on the registered site. For another domain, register that site separately in the dashboard (each site has its own key), or contact support.

<a id="AISX-A207"></a>
### AISX-A207 — Project is not active

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 403 | `project_inactive` | Search isn't available on this site. |

**Cause.** The project behind the key is not active (suspended, being deleted, or blocked by billing), or its site ownership / CMS plugin connection is no longer valid (for example the plugin was disconnected).

**Fix.** Check **Overview** and **Billing** in your dashboard. If the site uses the WordPress or Drupal plugin, open the plugin settings and reconnect.

<a id="AISX-A208"></a>
### AISX-A208 — Search index is not ready

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 409 | `not_ready` | Search is still being set up on this site. Please check back a little later. |

**Cause.** The site's search index is not ready yet — the first indexing is still running or has not finished successfully.

**Fix.** Wait for indexing to finish; **Overview** in your dashboard shows the status. The widget does not render at all until the index is ready ([AISX-W110](#AISX-W110)).

<a id="AISX-A209"></a>
### AISX-A209 — Subscription payment is overdue

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 402 | `project_payment_overdue` | AI search is currently unavailable on this site. Please try again later. |

**Cause.** The subscription is past due or paused (or has ended) for longer than the grace period, so AI search is switched off before any paid processing.

**Fix.** Open **Billing** in your dashboard and settle the payment or update the payment method. Search resumes once the subscription is active.

<a id="AISX-A210"></a>
### AISX-A210 — Too many requests

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 429 | `rate_limited` | Search is busy right now. Please wait N seconds and try again. |

**Cause.** Too many questions in a short time — from one visitor IP address (20 per minute) or for the whole site (per-site limit per minute). The `Retry-After` header says how many seconds to wait.

**Fix.** Usually transient: wait and retry. If you are load-testing or a bot is hammering the widget, slow down.

<a id="AISX-A211"></a>
### AISX-A211 — Daily safety cap reached

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 429 | `daily_cap_exceeded` | Search is unavailable for the rest of today. Please try again tomorrow. |

**Cause.** The site reached its daily ceiling of AI searches — a protection against a copied public key being abused. It resets at the next midnight UTC (`Retry-After` gives the seconds).

**Fix.** Wait for the reset. If your genuine traffic exceeds the cap, contact support from your dashboard.

<a id="AISX-A212"></a>
### AISX-A212 — Search quota used up

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 402 | `quota_exceeded` | A list of the most relevant pages on the site instead of an AI answer; when no pages can be listed: “AI search is temporarily unavailable. Please try again later.” |

**Cause.** The plan's search allowance for the current billing period and any prepaid extra searches are used up (the Free plan stops at its monthly limit). AI answers pause until more searches are available.

**Fix.** In **Billing**: buy extra searches, enable auto top-up, or upgrade the plan — or wait until the allowance renews at the start of the next billing period.

<a id="AISX-A213"></a>
### AISX-A213 — Quota check temporarily unavailable

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 503 | `quota_unavailable` | Search is temporarily unavailable. Please try again in a moment. |

**Cause.** The quota service could not be reached for this request. Nothing was charged.

**Fix.** Retry in a moment. If it persists, contact support from your dashboard.

<a id="AISX-A214"></a>
### AISX-A214 — The answer could not be generated

| HTTP status | `error` in the JSON response | What the visitor sees |
| --- | --- | --- |
| 502 | `answer_failed` | The answer couldn't be generated. Please try again. |

**Cause.** Generating the answer failed upstream. The search is not counted — it is returned to your quota.

**Fix.** Retry the question. If it keeps failing, contact support from your dashboard with the time of the request.

<a id="install-check-findings"></a>
## Install-check findings (AISX-C3xx)

Codes returned in `findings[].code` by the Achla AI installation check. The checker downloads one page as served (it does not run JavaScript) and compares it with the public widget configuration of the key it finds.

Run the check with `GET https://achlaai.com/api/install-check?url=<page URL>` (public, JSON, no key needed; results are cached for 60 seconds and rate-limited per IP), or with the **Check installation** button in the Achla AI dashboard (**Overview** and **Settings**, under the embed code). The response lists `findings` (`code`, `severity`, `title`, `cause`, `fix`, `docs`; `AISX-C301` also carries `detail` — the concrete reason the page could not be fetched) and the `limits` of the check.

Replace the URL with a page of your site that should show the widget.

```bash
curl -s "https://achlaai.com/api/install-check?url=https://YOUR-SITE.example"
```

| Code | Signal | Meaning |
| --- | --- | --- |
| [AISX-C301](#AISX-C301) | `page_unreachable` | The page could not be fetched |
| [AISX-C302](#AISX-C302) | `snippet_missing` | No widget tag in the page HTML |
| [AISX-C303](#AISX-C303) | `snippet_duplicate` | The widget tag appears more than once |
| [AISX-C304](#AISX-C304) | `key_missing` | The widget tag has no usable data-key |
| [AISX-C305](#AISX-C305) | `widget_disabled` | The widget is disabled for this key |
| [AISX-C306](#AISX-C306) | `inline_container_missing` | Inline mode without an #ai-search container |
| [AISX-C307](#AISX-C307) | `attach_selector_no_match` | The attach selector matches nothing in the page HTML |
| [AISX-C308](#AISX-C308) | `origin_mismatch` | The key belongs to a different site |
| [AISX-C309](#AISX-C309) | `ok` | Installation looks correct |
| [AISX-C310](#AISX-C310) | `foreign_script_host` | The widget script is loaded from an unexpected host |

<a id="AISX-C301"></a>
### AISX-C301 — The page could not be fetched

Finding id: `page_unreachable` (severity: error).

**Cause.** The checker could not download the page: DNS or connection error, timeout, a non-2xx HTTP status, a non-public address, or a firewall/WAF that blocks automated requests. The `detail` field of the finding names the actual reason (for example HTTP 403, a timeout, or a non-HTML response).

**Fix.** Make sure the URL opens publicly without a login. If a WAF blocks it, the browser checks below still work; see also the crawler page (`/en/bot`).

<a id="AISX-C302"></a>
### AISX-C302 — No widget tag in the page HTML

Finding id: `snippet_missing` (severity: error).

**Cause.** Neither a widget `<script>` (`/w.js`) nor a CMS plugin loader was found in the HTML as served. The checker does not run JavaScript, so a tag added by a tag manager or by client-side code is invisible to it.

**Fix.** Add the embed code from your dashboard before `</body>`. If you insert it through a tag manager or JavaScript, verify in the browser instead: the console must show no `[ai-search]` errors and the widget must appear. Also purge any page cache after adding the tag.

<a id="AISX-C303"></a>
### AISX-C303 — The widget tag appears more than once

Finding id: `snippet_duplicate` (severity: warn).

**Cause.** The page includes the widget more than once. Each copy mounts its own widget (two buttons, or [AISX-W105](#AISX-W105) for inline).

**Fix.** Keep exactly one tag per page — remove the copy from the theme, the tag manager, or the manual snippet if a plugin already adds it.

<a id="AISX-C304"></a>
### AISX-C304 — The widget tag has no usable data-key

Finding id: `key_missing` (severity: error).

**Cause.** The widget tag was found, but its `data-key` attribute is missing, empty, or malformed ([AISX-W101](#AISX-W101), [AISX-W112](#AISX-W112)).

**Fix.** Copy the embed code again from **Settings → Embed code** in your dashboard.

<a id="AISX-C305"></a>
### AISX-C305 — The widget is disabled for this key

Finding id: `widget_disabled` (severity: error).

**Cause.** `/api/widget-config` returns `{"enabled":false}` for the key on the page: the key is unknown or was deleted/replaced, the project is not active, the index is not ready, or the site connection is no longer valid ([AISX-W110](#AISX-W110)).

**Fix.** First compare the key on the page with **Settings → Embed code**; if they differ (the old key was deleted or replaced), replace the whole tag with the current one. The dashboard shows project, indexing and connection status; a stale or deleted key is not flagged there. If the keys match, open **Overview**.

<a id="AISX-C306"></a>
### AISX-C306 — Inline mode without an #ai-search container

Finding id: `inline_container_missing` (severity: error).

**Cause.** The widget is set to `inline`, but the page HTML has no element with `id="ai-search"`. Visitors get the floating button instead ([AISX-W104](#AISX-W104)).

**Fix.** Add `<div id="ai-search"></div>` where the search box should appear (WordPress: shortcode `[achla_search]` or the “Achla AI Search” block; Drupal: the “Achla AI Search” block), or switch placement to bubble.

<a id="AISX-C307"></a>
### AISX-C307 — The attach selector matches nothing in the page HTML

Finding id: `attach_selector_no_match` (severity: warn).

**Cause.** The widget is set to `attach`, but the selector does not match a text/search `<input>` in the HTML as served. Visitors get the floating button instead ([AISX-W103](#AISX-W103), [AISX-W106](#AISX-W106)). A field rendered by JavaScript is invisible to the checker.

**Fix.** Correct the selector so that it targets the search `<input>`; verify in the browser console with `document.querySelector('<selector>')`.

<a id="AISX-C308"></a>
### AISX-C308 — The key belongs to a different site

Finding id: `origin_mismatch` (severity: error).

**Cause.** The key on this page is registered for another site, so searches from this page are rejected with `origin_not_allowed` ([AISX-A206](#AISX-A206)).

**Fix.** Use the key of the project registered for this site, or register this site separately in the dashboard.

<a id="AISX-C309"></a>
### AISX-C309 — Installation looks correct

Finding id: `ok` (severity: info).

**Cause.** The tag is present once, the key is enabled, and the placement requirements are met in the page HTML.

**Fix.** Nothing to fix. Open the page in a browser and ask a test question to confirm an answer with sources.

<a id="AISX-C310"></a>
### AISX-C310 — The widget script is loaded from an unexpected host

Finding id: `foreign_script_host` (severity: warn).

**Cause.** The `w.js` tag points to a host other than the official widget origin — for example a self-hosted or CDN-cached copy. The widget derives its API address from its own script URL, so such a copy calls the wrong server and does not receive updates.

**Fix.** Load the script exactly as in the embed code from your dashboard; do not copy `w.js` to your own server or CDN.

<a id="indexing"></a>
## Search does not know a page

- **Plan page limit.** Each plan indexes up to a fixed number of pages; pages beyond the limit are not indexed. Check the plan in **Billing**.
- **robots.txt and firewalls.** The Achla AI crawler respects `robots.txt`. A WAF or bot protection that blocks it leaves pages unindexed — see the [crawler page](https://achlaai.com/en/bot) for how to identify and allow it.
- **JavaScript-only content.** Pages whose text is rendered only in the browser may be indexed with little or no text.
- **New or changed pages** are picked up only when the site is re-crawled, not instantly.

<a id="help"></a>
## Still stuck

Open **Support** in the Achla AI dashboard and include: the page URL, the exact `[ai-search]` console lines (with their `AISX-…` codes), and the output of the `widget-config` command above.
