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

# Model Tier Gate

> Why a tool your credential is scoped for can still return -32003, and how tier equivalence resolves it

# Model tier gate

An organization can require that a specific tool only runs behind a capable
model — "this action needs a pro-or-better model". NexSpace enforces that floor
on every surface, including MCP. Because your model is invisible to NexSpace,
an external caller is graded by a **credential-level tier equivalence** instead.

The gate is **fail-closed**: if the tool carries a floor and your credential has
no equivalence configured, the call is refused. The same tool call succeeds for
the in-app assistant, which is why this is the most confusing `-32003` on the
surface.

<Note>
  Tier floors are **opt-in per tool** and no tool ships with one. If your
  organization has never set a floor in AI Configuration → Tool Management,
  nothing on this page changes anything for you: every tool resolves to "no
  floor" and passes untouched.
</Note>

## What a model-tier floor is

A NexSpace operator sets a per-tool floor in **AI Configuration → Tool
Management**. The effective floor for a tool resolves as:

1. the platform-store override (`ai_tool_definitions.required_model_tier`), then
2. the tool's own `requiredModelTier` declared in code, then
3. no floor.

The in-app agent loop compares that floor against the tier of the model actually
running the turn. Over MCP there is no such thing to compare: the model belongs
to **your** client — claude.ai, ChatGPT, Claude Code, a script — and NexSpace
never sees it and cannot grade it. Before this gate existed, connector traffic
simply bypassed tier floors, which made external callers the one surface exempt
from the control.

## Tier equivalence

A **tier equivalence** is the tier a headless credential's calls are *treated*
as running at. It is a property of the credential, not of the request.

There are four tiers, ranked lowest to highest:

| Tier | Rank |
| - | - |
| `fast` | 0 |
| `basic` | 1 |
| `pro` | 2 |
| `max` | 3 |

A credential with no equivalence anywhere is **ungraded** (`null`).

## Resolution order

The equivalence resolves most-specific-first. The first source that yields a
valid tier wins:

| Order | Source | Keyed by |
| - | - | - |
| 1 | `HEADLESS_MCP_TIER_BY_KEY` | the API key's id |
| 2 | `HEADLESS_MCP_TIER_BY_FACILITY` | the API key's facility, else the bound user's facility |
| 3 | `HEADLESS_MCP_TIER_EQUIVALENCE` | fleet-wide default |
| 4 | — | nothing matched → **ungraded** (`null`) |

The two maps are JSON objects of the form `{ "<id>": "<tier>" }`. All three are
environment configuration an operator sets — there is no API for them, and no
request header or tool argument can influence the result.

<Warning>
  An OAuth token has no API key id, so **source 1 can never match for an OAuth
  connector**. An OAuth credential is graded only by its bound user's facility
  (source 2) or by the fleet default (source 3). If you are debugging a connector
  that is ungraded while an API key on the same facility is graded, that is the
  reason.
</Warning>

Malformed JSON in either map, or an entry whose value is not one of the four
tiers, is dropped silently and resolution falls through to the next source. A
bad env var never throws on the tool-call path — it just makes credentials less
graded, never more.

## Fail-closed semantics

Given a tool's effective floor and the credential's equivalence:

| Tool floor | Equivalence | Result |
| - | - | - |
| none | anything, including ungraded | **passes** — the normal case |
| any floor | ungraded (`null`) | **blocked** |
| `pro` | `fast` or `basic` | **blocked** — equivalence ranks below the floor |
| `pro` | `pro` or `max` | passes |

Two consequences worth internalizing:

* **An ungraded credential is blocked by *any* floor**, even a `fast` floor. It
  is not "assume the lowest tier"; it is "no standing at all".
* **An org that never set a floor sees no change.** The gate is narrow by
  design — it only bites where an operator explicitly asked for a capable model.

## Where it runs

The gate is applied in both dispatch paths:

* **Named verbs** — checked against the verb id (`fillShift`, `runPayroll`, …).
  Verbs declare no code-level tier, so a verb only carries a floor when an
  operator set an override row for that verb id.
* **Registry tools** — checked against the tool name and its code-declared
  `requiredModelTier`, override taking precedence.

<Warning>
  The tier gate is **independent of** scopes, guardrails, approvals, and the
  tenant-boundary guard — different mechanism, different error payload. In the
  dispatch order it runs **after** the credential's scope check (a missing scope
  is reported first, as `Insufficient credential scope`) and **before** the
  [tenant-boundary guard](/concepts/tenant-isolation), the
  [org guardrails](/concepts/governance), the idempotency reservation, and the
  approval gate. So a floored tool is refused before any guardrail is evaluated
  and before an `Idempotency-Key` is consumed — a tier rejection never wedges a
  key.
</Warning>

## The error you get

A blocked call returns JSON-RPC `-32003` (FORBIDDEN). The `message` names the
tool and the required tier; `error.data` carries `requiredTier` and
`tierEquivalence` so you can branch on it without parsing prose.

**Ungraded credential:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32003,
    "message": "Tool move_deal_stage requires the \"pro\" model tier or higher, and this credential has no tier equivalence configured. An operator can grant one via HEADLESS_MCP_TIER_BY_KEY / HEADLESS_MCP_TIER_BY_FACILITY / HEADLESS_MCP_TIER_EQUIVALENCE, or lower the tool's tier floor in AI Configuration → Tool Management.",
    "data": {
      "requiredTier": "pro",
      "tierEquivalence": null,
      "suggestion": "Ask a NexSpace operator to grant this credential a model-tier equivalence, or use a tool without a tier floor.",
      "retryable": false
    }
  }
}
```

**Under-graded credential:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 8,
  "error": {
    "code": -32003,
    "message": "Tool runPayroll requires the \"max\" model tier or higher; this credential's tier equivalence is \"basic\". Raise the credential's equivalence or lower the tool's tier floor in AI Configuration → Tool Management.",
    "data": {
      "requiredTier": "max",
      "tierEquivalence": "basic",
      "suggestion": "Ask a NexSpace operator to grant this credential a model-tier equivalence, or use a tool without a tier floor.",
      "retryable": false
    }
  }
}
```

`tierEquivalence` is `null` exactly when the credential is ungraded, and a tier
string when it is graded but too low — that single field tells you which of the
two remediations you need.

## How to tell this apart from the other -32003s

`-32003` is returned for several unrelated reasons. Branch on `error.data`:

* `requiredTier` / `tierEquivalence` present → **this page**.
* `required` / `granted` present → a scope gap. See
  [MCP](/concepts/mcp#how-scopes-are-matched-over-mcp).
* `guardrail` present → an org guardrail. See
  [Governance](/concepts/governance).
* `suggestion` reads "Omit facilityId to use your accessible facilities…" → a
  tenant-boundary violation. See
  [Tenant isolation](/concepts/tenant-isolation).

## Remediation

`retryable` is `false` and it means it — the same call will fail forever until
configuration changes. Two ways forward:

1. **Ask a NexSpace operator to grant this credential a model-tier
   equivalence**, at whichever level of the resolution order fits (per key, per
   facility, or fleet-wide).
2. **Use a tool without a tier floor.** The floor is per tool, so an
   alternative tool in the same category usually works.

An operator can also lower the tool's floor in AI Configuration → Tool
Management, which changes it for every surface — the in-app assistant included —
so it is the heavier of the two changes.

## See also

* [MCP](/concepts/mcp#model-tier-floors) — where this gate sits in the tool-call
  path.
* [Governance](/concepts/governance) — guardrails and the headless autonomy
  dial, the other two operator controls that change how your calls behave.
* [Tenant isolation](/concepts/tenant-isolation) — the facility-boundary guard.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.