# Access errors (/api/errors)

A call made with an API token (`pltu_`, `plto_`, `pltd_`) or an OAuth access token is decided by Poltura's
gateway against the grant behind it. A refusal always has the same JSON body:

```json
{
  "error": "permission_denied",
  "message": "this token does not allow orders.write on store:0199f3a2-7c41-7d2e-9b1a-3c5e8f0a1b2c",
  "permission": "orders.write",
  "resource": "store:0199f3a2-7c41-7d2e-9b1a-3c5e8f0a1b2c",
  "documentation_url": "https://developers.poltura.com/api/errors#permission-denied"
}
```

`permission` and `resource` appear when the refusal is about one permission on one store or organisation.

## invalid-token [#invalid-token]

`401`. The credential is unknown, malformed, expired, revoked, disabled or exposed. The response carries
`WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…"`; an OAuth client starts a new
authorization there, a token holder creates or rolls the token.

## invalid-dpop-proof [#invalid-dpop-proof]

`401`. The credential is a Poltura CLI sign-in, which is bound to a key ([DPoP](/apps/cli#sign-in-or-use-a-token)),
and the request did not prove it holds that key. The proof is missing, signed by another key, made for another
method, URL or token, too old, or already used. The token was sent as `Bearer` instead of `DPoP`, or another
credential was sent with the `DPoP` scheme. The response carries `WWW-Authenticate: DPoP error="invalid_dpop_proof"`.
Use the CLI on the machine that signed in, or run `poltura login` again.

## use-dpop-nonce [#use-dpop-nonce]

`401` (`400` from the token endpoint). The proof's time is too far from the server's. The response carries a
`DPoP-Nonce` header: put that value in the proof's `nonce` claim and send the request again. The CLI does
this itself.

## insufficient-scope [#insufficient-scope]

`403`. An OAuth grant lacks the permission, and asking for it again can help. The challenge lists every
permission the grant already holds plus the missing one:

```
WWW-Authenticate: Bearer error="insufficient_scope", scope="orders.read products.read orders.write",
  resource_metadata="…"
```

Re-authorize with exactly that `scope`. A grant that already holds the permission is never challenged again.

## permission-denied [#permission-denied]

`403`. The token's policy does not allow the permission, the grant does not reach this store, or the
person who owns the access no longer holds the permission in their role. No new consent changes that.

## not-delegable [#not-delegable]

`403`. The permission can never be used through a credential: it needs a second factor, belongs to an owner
only, manages access itself, or moves money. Do it in the admin.

## ip-not-allowed [#ip-not-allowed]

`403`. The token is limited to addresses this call did not come from.

## access-disabled [#access-disabled]

`403`. The organisation switched off third-party apps and agents, the store switched off AI agents for
`/mcp`, the client was blocked, or the store or the grant is suspended.

## approval-required [#approval-required]

`409`. An agent action is waiting for a person to approve it. See [Approval](/ai/agents#approval).

## request-too-costly [#request-too-costly]

`400`. One call would cost more than 100 points, for example a list with a very large `limit` and many includes. It
was not run. Ask for a smaller page or fewer includes. The costs are on
[How agents act on your store](/ai/agents#rate-limits).

## rate-limited [#rate-limited]

`429`. The points budget of this credential, of this client at this store, or of the store as a whole is used up.
The body names the `bucket` that ran out and `retryAfterMs`, and the response carries `Retry-After` in seconds and
the `X-Poltura-RateLimit-*` headers. The call did not reach the store, so waiting that long and sending it again is
safe. Inside an MCP tool call the same refusal is a tool error with the code `rate_limited`.

## unavailable [#unavailable]

`503`. Poltura could not decide the call just now. Retry after `Retry-After` seconds; nothing was done.
