> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tappify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error code an extension can see, what it means, and what to do about it.

This is the reference for every code an extension can be handed. Each carries a stable `code`
and a sentence you can act on. Errors from the SDK and the bridge are `TapError`; a failure your
own server returned reaches your component as a `TapServerError` carrying your own code and
message; Tappify's own refusals arrive with the codes in the runtime, publish and load-failure
tables below.

```ts theme={null}
import { TapError, TapServerError } from "@tappify/extension-sdk";

if (TapError.is(error) && error.code === "TAP_SCOPE_MISSING") {
  // render the "not shared with this extension" state
}
```

`TapError.is` and `TapServerError.is` are the narrowing you want: both check the error's own
name and code rather than `instanceof`, so they hold across a bundle boundary.

## In your component

| Code                     | Means                                                                                                                 | Fix                                                                                                    |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `TAP_OUTSIDE_HOST`       | `useTap()` ran outside a Tappify mount                                                                                | Render the component from an entry your manifest declares, or wrap it in `renderWithTap()`             |
| `TAP_NO_QUERY_CLIENT`    | A data hook ran with no host query client                                                                             | Use `renderWithTap()` in tests; inside the host this cannot happen                                     |
| `TAP_NO_PORTAL`          | `TapDialog` rendered with no portal node                                                                              | Render the dialog from a contribution entry, not through a portal of your own                          |
| `TAP_SCOPE_MISSING`      | A data query, a storage call or a procedure needs a scope the install did not grant                                   | Ask `tap.auth.can(scope)` first, and render a state that explains it                                   |
| `TAP_UNKNOWN_COLLECTION` | The name is not a declared storage collection, or `useTapStorage` was pointed at a collection that is not a singleton | Declare it under `contributes.storage`, or set `"singleton": true`, then run `tappify extension types` |
| `TAP_UNKNOWN_PROCEDURE`  | No procedure with that name in the live release                                                                       | Declare it under `server.procedures`, then run `tappify extension types`                               |

`tap.actions.run` resolves with the run Tappify created and rejects with a `TapServerError`
carrying one of the `ACTION_*` codes below when Tappify refused to create one.

## In your build

| Code                    | Means                                                                                                            | Fix                                                                                       |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `TAP_MANIFEST_INVALID`  | The build read a manifest that fails the schema                                                                  | Run `tappify extension doctor` for the same list with suggested fixes                     |
| `TAP_ENTRY_MISSING`     | A contribution's `entry` file is not on disk, or the array passed to Vite did not come from `tappifyExtension()` | Create the file, remove the contribution, or pass the array `tappifyExtension()` returned |
| `TAP_SCHEMA_UNRESOLVED` | The manifest points at a schema file the type generator was not given a way to read                              | Run `tappify extension types`, which reads schema files from the extension directory      |
| `TAP_STYLES_UNRESOLVED` | Reserved for a stylesheet the build cannot collect                                                               | Nothing throws it today; import the stylesheet from the entry file itself                 |

## On your server

`createTappifyHandler` answers these as JSON with the code and the sentence, so you rarely
construct one yourself.

| Code                   | Means                                                                                        | Fix                                                                                                                           |
| ---------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `TAP_TOKEN_MISSING`    | A call arrived with no `Authorization: Bearer` header                                        | Forward the header unchanged from your framework; only Tappify calls these routes                                             |
| `TAP_TOKEN_INVALID`    | The token did not verify against the key set, or it is missing a claim                       | Check that your clock is right and that `jwksUrl` points at the Tappify environment calling you                               |
| `TAP_TOKEN_AUDIENCE`   | The token was minted for another extension                                                   | Build the handler with the same `extensionId` as the `id` in your manifest                                                    |
| `TAP_EVENT_ID_MISSING` | A call arrived with no `X-Tappify-Event-Id`                                                  | Forward the header unchanged, and use it as the key when you deduplicate                                                      |
| `TAP_BODY_INVALID`     | The body was not the JSON the route expects, or a framework parser consumed the stream first | Mount `express.json()` before the Tappify middleware, or the middleware before any parser                                     |
| `TAP_BODY_TOO_LARGE`   | Over 1 MB in                                                                                 | Return a cursor or an id the host can follow instead of inlining the payload                                                  |
| `TAP_ROUTE_UNKNOWN`    | Your server was asked for a path it does not route                                           | Check the method and the path, and set `basePath` if your framework mounts the handler under a prefix                         |
| `TAP_HANDLER_MISSING`  | Tappify called a route you declared but did not implement                                    | Add the handler to `createTappifyHandler`, or remove the contribution from the manifest                                       |
| `TAP_INTERNAL_ERROR`   | Your handler threw something that is not a `TapServerError`                                  | Catch it and throw a `TapServerError` with a code and a sentence                                                              |
| `TAP_WEBHOOK_REJECTED` | Tappify refused an event `sendEvent` posted                                                  | Check the inbound signing secret on your extension's Server page, and that the event is declared under `contributes.webhooks` |

## In your tests

| Code                      | Means                                                                                                                            | Fix                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `TAP_TEST_TARGET_INVALID` | A matcher was given something other than the mock `renderWithTap()` returned, or `createTestClient()` was handed a built handler | Call matchers as `expect(mock).…`, and pass `createTestClient` the handler options |

## Runtime errors from Tappify

These arrive on a bridge call. The host turns a `SCOPE_NOT_GRANTED` into `TAP_SCOPE_MISSING`
before your component sees it; the rest reach you with the code below.

| Code                          | Means                                                                                                                                                            | Fix                                                                                                                 |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `EXTENSIONS_DISABLED`         | The extension runtime is off for that workspace                                                                                                                  | Nothing on your side; the workspace has not been enabled                                                            |
| `INSTALL_HEADER_MISSING`      | A bridge call reached Tappify without its install header                                                                                                         | Only the host makes these calls                                                                                     |
| `INSTALL_NOT_FOUND`           | No install with that id belongs to that workspace                                                                                                                | Reload; the install was removed                                                                                     |
| `INSTALL_NOT_ACTIVE`          | The install is paused or retired                                                                                                                                 | The owner resumes it from the install's page                                                                        |
| `EXTENSION_SUSPENDED`         | Tappify suspended the extension                                                                                                                                  | Contact Tappify; it stops loading everywhere within a minute                                                        |
| `EXTENSION_RETIRED`           | The extension was retired by its vendor                                                                                                                          | Nothing installs or opens; the vendor publishes a replacement under a new id                                        |
| `SCOPE_NOT_GRANTED`           | The route needs a scope the install did not grant                                                                                                                | Ask for it in your manifest and let the owner review scopes                                                         |
| `RATE_LIMIT_EXCEEDED`         | The install used its allowance for that surface                                                                                                                  | See [Storage limits and rate limits](/extensions/reference/limits)                                                  |
| `QUERY_KIND_UNSUPPORTED`      | The data API does not serve that query kind                                                                                                                      | Use one of the seven kinds on [Scopes](/extensions/reference/scopes)                                                |
| `STORAGE_COLLECTION_UNKNOWN`  | The live release declares no such collection                                                                                                                     | Declare it and publish                                                                                              |
| `STORAGE_DOCUMENT_INVALID`    | The document does not match the collection schema                                                                                                                | Fix the document, or widen the schema and publish                                                                   |
| `STORAGE_DOCUMENT_TOO_LARGE`  | Over 256 KB                                                                                                                                                      | Split the document                                                                                                  |
| `STORAGE_COLLECTION_FULL`     | 1,000 documents for that collection and scope key                                                                                                                | Delete documents you no longer need                                                                                 |
| `STORAGE_QUOTA_EXCEEDED`      | 50 MB for the install                                                                                                                                            | Delete documents you no longer need                                                                                 |
| `DOCUMENT_NOT_FOUND`          | No document with that id is in the collection                                                                                                                    | Check the id, or `list` first                                                                                       |
| `PROCEDURE_NOT_DECLARED`      | The live release declares no such procedure                                                                                                                      | Declare it and publish                                                                                              |
| `PROCEDURE_INPUT_INVALID`     | The input failed your declared input schema                                                                                                                      | Fix the call, or widen the schema                                                                                   |
| `PROCEDURE_OUTPUT_INVALID`    | Your server returned a body that fails your output schema                                                                                                        | Fix the handler, or widen the schema                                                                                |
| `PROCEDURE_PAYLOAD_TOO_LARGE` | Over 1 MB in or out                                                                                                                                              | Paginate                                                                                                            |
| `PROCEDURE_TIMEOUT`           | Your server did not answer in 30 seconds                                                                                                                         | Make the call faster, or move the work behind an event                                                              |
| `PROCEDURE_FAILED`            | Your server returned an error, or could not be reached                                                                                                           | Read your own logs; the message reaches the UI                                                                      |
| `SERVER_NOT_DECLARED`         | The extension has no `server.baseUrl`, so it has no procedures                                                                                                   | `tappify extension set server.baseUrl <url>`                                                                        |
| `TELEMETRY_NAME_UNKNOWN`      | The name is not in the live release's `telemetry` list                                                                                                           | Declare it and publish                                                                                              |
| `TOKEN_SIGNING_UNAVAILABLE`   | That environment has no install-token signing key                                                                                                                | Contact Tappify                                                                                                     |
| `METRIC_NOT_DECLARED`         | A series read named a metric the live release does not declare                                                                                                   | Add it under `contributes.connector.metrics` and publish                                                            |
| `METRICS_RESPONSE_INVALID`    | A sync response broke the contract                                                                                                                               | Read the run on your Runtime page; it names the first three problems                                                |
| `CONNECTOR_NOT_DECLARED`      | Sync health was asked for on an extension with no connector                                                                                                      | Declare a connector, or stop asking                                                                                 |
| `WEBHOOK_SIGNATURE_INVALID`   | The signature does not match the body under this install's secret                                                                                                | Sign the exact bytes you send; check the secret on your Server page                                                 |
| `WEBHOOK_ENVELOPE_INVALID`    | A field is missing, or `source` is not your extension id                                                                                                         | Send `{ event, source, occurredAt, dedupeKey, payload }`                                                            |
| `WEBHOOK_EVENT_NOT_DECLARED`  | The event name is not in the live release                                                                                                                        | Add the webhook and publish                                                                                         |
| `WEBHOOK_PAYLOAD_INVALID`     | The payload does not match the declared schema                                                                                                                   | Check `why` is present for an alert or a banner                                                                     |
| `WEBHOOK_EVENT_NOT_FOUND`     | No stored delivery with that id belongs to this install                                                                                                          | Replay a delivery the install's list shows                                                                          |
| `ALERT_LIMIT_REACHED`         | An alert was replayed after the project had spent its three for the day. A fourth live delivery never raises this: it is stored as rate-limited and answered 202 | Replay it tomorrow, and batch the day's findings into one alert                                                     |
| `ACTION_NOT_DECLARED`         | The action id is not in the live release                                                                                                                         | Add the action and publish                                                                                          |
| `ACTION_INPUT_INVALID`        | The input does not match the declared schema                                                                                                                     | Build the form from the schema with `TapForm`                                                                       |
| `ACTION_RUN_NOT_FOUND`        | No run with that id belongs to this install                                                                                                                      | Read the id back from the run `tap.actions.run` returned                                                            |
| `ACTION_RUN_ALREADY_DECIDED`  | A run was approved or rejected twice                                                                                                                             | Read the run's status before deciding                                                                               |
| `ALWAYS_ASK_REQUIRED`         | An owner tried to stop being asked on an install holding a scope that needs a security review                                                                    | Nothing; this cannot be turned off                                                                                  |
| `TOOL_NOT_DECLARED`           | The assistant asked for a tool id the live release does not declare                                                                                              | Declare the tool and publish                                                                                        |
| `TOOL_INPUT_INVALID`          | The arguments the assistant passed do not match the tool's `input` schema                                                                                        | Widen the schema, or describe its fields so the assistant can fill them                                             |
| `TOOL_RESPONSE_INVALID`       | Your answer is missing a key its `returns` promises, or a context provider answered with something other than `{ summary, data? }`                               | Check a tool's answer with `validateToolResponse` in your own tests                                                 |
| `TOOL_FAILED`                 | Your server answered a tool, mention or context call with a non-2xx                                                                                              | Read your own logs; a tool or context call also leaves an error row against the install, a mention call leaves none |
| `MENTION_NOT_DECLARED`        | The `@` picker asked for a mention list the live release does not declare                                                                                        | Declare the mention list and publish                                                                                |
| `MENTION_RESPONSE_INVALID`    | A mention list answered with something other than `{ items: [{ id, label }] }`                                                                                   | Answer with that shape                                                                                              |
| `CONTEXT_NOT_DECLARED`        | A context provider was asked for by an id the live release does not declare                                                                                      | Declare the provider and publish                                                                                    |
| `PROMPT_NOT_DECLARED`         | A chip named a prompt the live release does not declare                                                                                                          | Declare the prompt and publish; an open chat outlives a release that dropped one                                    |
| `CREDENTIALS_UNREADABLE`      | The stored connector credentials could not be read back                                                                                                          | The owner reconnects the extension from its page                                                                    |
| `OAUTH_NOT_DECLARED`          | Connect was pressed on a connector that does not use OAuth                                                                                                       | Declare `auth.method: "oauth"` with its urls and scopes                                                             |
| `OAUTH_STATE_INVALID`         | The connection link expired or was reused                                                                                                                        | Press Connect again                                                                                                 |
| `OAUTH_EXCHANGE_FAILED`       | The provider refused the code                                                                                                                                    | Check the client id and secret the owner entered                                                                    |

None of the eight assistant codes reaches a component of yours, and none of them is shown to the
owner as an error. What each one leaves behind differs:

* A `TOOL_*` refusal ends one tool call. The assistant is told the tool did not answer and carries
  on with the turn, and the call is recorded against the install as a `tool_call` with an error
  outcome.
* `CONTEXT_NOT_DECLARED`, or a `TOOL_FAILED` on the context route, drops that context block in
  silence: the assistant is told nothing, and the call is recorded as a `context_call` with an
  error outcome.
* A `MENTION_*` refusal costs the owner the items from that extension in the `@` picker. It never
  reaches the assistant, because the picker is the owner's own, and no runtime row is written.
* `PROMPT_NOT_DECLARED` costs the `prompt_sent` record, not the turn: the owner's question is
  answered as they sent it, and no runtime row is written.

The two kinds of row that are written are counted by type on your Runtime page, and listed with
their outcome in the owner's install activity.

## Publish and account errors

| Code                       | Means                                                                                  | Fix                                                                                     |
| -------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `VENDOR_WORKSPACE_MISSING` | You are not signed in to a workspace                                                   | `tappify login`, then pick a workspace                                                  |
| `SANDBOX_PROJECT_MISSING`  | Your vendor has no sandbox project                                                     | `tappify vendor create`                                                                 |
| `EXTENSION_NOT_FOUND`      | No extension with that id belongs to your vendor                                       | `tappify extension publish` or `tappify extension dev --live`; either registers the id  |
| `EXTENSION_ID_TAKEN`       | An extension already has that id                                                       | `tappify extension set id <new-id>` and publish again                                   |
| `EXTENSION_ID_RESERVED`    | The id would shadow a Tappify route                                                    | `tappify extension set id <new-id>` and publish again                                   |
| `MANIFEST_INVALID`         | The manifest failed validation                                                         | `tappify extension doctor`                                                              |
| `BUNDLE_INVALID`           | The bundle failed validation, which includes a skill file past 4,000 words             | `tappify extension doctor`                                                              |
| `KNOWLEDGE_FILE_MISSING`   | A declared knowledge file is not in the bundle                                         | Check the path and publish again; nothing was written, so the live release is untouched |
| `SKILL_FILE_MISSING`       | A declared skill file is not in the bundle                                             | Check the path and publish again; nothing was written, so the live release is untouched |
| `BUNDLE_TOO_LARGE`         | Over 5 MB                                                                              | Trim `dist/`; large images and bundled fonts are the usual cause                        |
| `SERVER_HEALTH_FAILED`     | A declared server did not answer `GET /tappify/health` with 200 within 3 seconds       | Deploy the server the message names, then publish again                                 |
| `RELEASE_NOT_FOUND`        | No release in that state                                                               | `tappify extension status`                                                              |
| `RELEASE_NOT_IN_REVIEW`    | Nothing is waiting for review to withdraw                                              | `tappify extension status`                                                              |
| `NO_LIVE_RELEASE`          | Nothing to install yet                                                                 | Publish first                                                                           |
| `VISIBILITY_FORBIDDEN`     | The extension is not available to that workspace, or the listing cannot widen that far | Check [visibility](/extensions/publish/visibility)                                      |
| `DEV_SESSION_NOT_FOUND`    | You have no dev session for that extension                                             | `tappify extension dev --live`                                                          |
| `EVENT_NOT_REPLAYABLE`     | That runtime row records something the host did, not a delivered event                 | Replay a delivered event instead                                                        |
| `CDN_NOT_CONFIGURED`       | That environment has no bundle store                                                   | Contact Tappify                                                                         |

## Load failures

When a bundle cannot be mounted, the host draws its failure card and an owner can send the
reason to your Runtime page. These are the codes that reach you there.

| Code                 | Means                                                                 |
| -------------------- | --------------------------------------------------------------------- |
| `REMOTE_MISSING`     | The install has no published bundle                                   |
| `ORIGIN_NOT_ALLOWED` | The entry's origin is not on the extension allowlist                  |
| `INTEGRITY_MISMATCH` | The bundle does not match the SHA-384 hash recorded for the release   |
| `REMOTE_TIMEOUT`     | `remoteEntry.js` did not answer within 8 seconds                      |
| `REMOTE_FAILED`      | The entry answered a non-200, or the remote threw while loading       |
| `MODULE_MISSING`     | The bundle has no default export at the expose the contribution names |

## What the CLI shows

A failed command prints what failed, why, and the fix, and never a stack. For most codes the
"why" is the sentence Tappify returned. The fix is a command where one exists —
`tappify login`, `tappify vendor create`, `tappify extension set id`, `tappify extension doctor`
and `tappify extension publish` are the ones it names most often — and a sentence where the fix
is not yours to run, such as asking a vendor admin or asking Tappify support. A code with no
tailored fix falls back to `tappify extension doctor`. Add `--json` to any
`tappify extension` or `tappify vendor` command and the same failure arrives as
`{ "ok": false, "error": { "code", "what", "why", "fix" } }`.

## Writing your own

```ts theme={null}
throw new TapServerError(
  "RANGE_TOO_SHORT",
  "getSummary needs a range of at least one day. Widen the date picker, then retry.",
  400,
);
```

The code is stable and the message is what your component shows the owner, so write a sentence
that says what happened and what to do. A `TapServerError` your handler throws reaches the
extension with your own code and message intact; anything else your handler throws reaches it as
`TAP_INTERNAL_ERROR` with Tappify's sentence, and the detail stays in your logs.

<Card title="Scopes" icon="key" href="/extensions/reference/scopes">
  What `TAP_SCOPE_MISSING` and `SCOPE_NOT_GRANTED` are asking you to declare.
</Card>
