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

# Safety, Scopes & Errors

> How tool calls are authorized, how org isolation is enforced, and the structured error contract every tool shares

## Scope enforcement

Every tool call is checked against the caller's token **once**, before the tool body runs, using the same scope groups as the REST API (see [Scopes](/authentication#scopes)):

* `READ`-class tools require a CRM read (or manage) scope.
* `WRITE` and `COST_BEARING`-class tools require a CRM manage scope.

This mirrors the REST routers exactly — a `viewer`-role credential that only carries read scopes gets rejected on any write tool, with no silent downgrade. Because the check is derived from the tool's declared class rather than a second, independently-set flag, it can't drift out of sync with what the tool actually does.

## Org isolation is structural, not per-tool code

A tool's arguments never include an organization id, and a client-supplied `organization_id`-shaped field in the call arguments is simply ignored if you try to pass one — the caller's organization comes entirely from the credential (fixed on an `ak_` key, or supplied via the `X-Anyreach-Org` header for a `pat_` token and re-verified against Logto on every connection). Every downstream data access is additionally scoped by Postgres row-level security keyed off that same organization claim. There is no tool argument that can widen a call's reach to another organization's data.

## Name and ID resolution

Most tools that take a record reference (a deal, account, or contact) accept either its UUID **or** a plain name, and resolve it server-side:

* An exact, case-insensitive name match resolves immediately.
* Multiple plausible matches return a structured `ambiguous` error with up to 8 candidates, rather than guessing which one you meant.

This is deliberate: guessing here would risk writing to the wrong customer's record. If you get an `ambiguous` error, re-issue the call with the UUID from the candidate list.

## Idempotency

Exactly five tools accept an optional `idempotency_key`, and they are the ones that create a record or start a paid job: `create_account`, `create_contact`, `create_deal`, `log_activity`, and `cx_start_run`. Retrying the same call with the same key returns the original result instead of repeating the effect; if you omit the key, one is derived by hashing the normalized call arguments, so an exact retry is still caught.

<Note>This is a best-effort, in-process cache (entries expire after 24 hours), not a distributed guarantee. It exists to stop the realistic failure mode — an agent retrying a call within one conversation — not to guarantee exactly-once semantics across a restart or a genuinely concurrent duplicate request. For a Customer Intelligence run, a retry that evades this cache means spending provider money twice.</Note>

<Warning>The **(idempotent)** marker in [Available Tools](/mcp/tools) means something else entirely: those tools are *naturally* idempotent (repeating one converges on the same state), which is only a hint passed to your MCP client. None of them take an `idempotency_key` — an `idempotency_key` sent to a tool outside the five above is silently discarded as an unknown argument, and the write, along with its stage history, notifications and follow-up workflows, runs again.</Warning>

## Superadmin-gated tools

A handful of Customer Intelligence tools and fields are restricted to AnyCRM's own platform operators, independent of any scope a customer's credential can carry:

* `cx_list_runs`, `cx_get_run`, `cx_start_run`, and `get_job` redact cost/spend figures for anyone who isn't a superadmin, rather than refusing the call outright.
* `cx_get_result_metrics` refuses non-superadmins entirely.
* `cx_set_manual_demo` and `cx_restore_cx_report` are superadmin-only.
* `cx_edit_cx_report` / `cx_preview_cx_report` gate a few specific fields (competitive analysis, brand data) to superadmins while leaving the rest of the report editable by anyone with CRM manage scope.

These checks are keyed on the caller's email domain matching AnyCRM's own operator domain — no organization API key or personal access token issued to a customer can satisfy them.

## Limits

| Limit            | Value             | Behavior when exceeded                                                                                                                                                        |
| ---------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Per-call timeout | 20 seconds        | Returns a structured `timeout` error (`retryable: true`) rather than leaving the connection open — well inside the \~60s most MCP clients wait before giving up on their own. |
| Result size      | 48 KiB serialized | The result is withheld (not truncated mid-JSON) and replaced with a `too_large` error (`retryable: true`) telling you to narrow the filter or lower `limit`.                  |

## Error shape

Every tool failure — a scope check, a validation error, a timeout, an oversized result, an ambiguous reference, anything — comes back as a structured object rather than a transport-level failure, so a model can read `error.code` and decide what to do next:

```json theme={null}
{
  "error": {
    "code": "insufficient_scope",
    "message": "Missing a manage scope (e.g. deals:manage)",
    "retryable": false
  }
}
```

| Code                 | Meaning                                                                                                | Retryable                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unauthenticated`    | Missing or invalid credential.                                                                         | No                                                                                                                                                                            |
| `insufficient_scope` | Valid credential, missing scope (or a superadmin-only action).                                         | No                                                                                                                                                                            |
| `validation`         | Arguments failed validation.                                                                           | No                                                                                                                                                                            |
| `not_found`          | The referenced record doesn't exist, or isn't visible to this organization.                            | No                                                                                                                                                                            |
| `ambiguous`          | A name reference matched more than one record — see [Name and ID resolution](#name-and-id-resolution). | No — retry with a specific id                                                                                                                                                 |
| `conflict`           | The requested change conflicts with the record's current state.                                        | No                                                                                                                                                                            |
| `timeout`            | The call exceeded its time budget.                                                                     | Yes                                                                                                                                                                           |
| `too_large`          | The result exceeded the size budget and was withheld.                                                  | Yes — narrow the request                                                                                                                                                      |
| `rate_limited`       | Too many calls in a short window.                                                                      | Yes                                                                                                                                                                           |
| `internal`           | An unexpected server-side failure.                                                                     | Read the field: `true` for an unhandled exception, `false` when it came from an upstream status the mapper has no code for (e.g. a `502` from a workflow that wouldn't start) |

`not_found` is also what you get for a record that exists but belongs to a different organization — row-level security makes the two cases indistinguishable on purpose, the same way the REST API never confirms a cross-org record exists via a `403` (see [Errors & Pagination](/errors-and-pagination)).
