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

# Errors and Limits

> Error shapes and status codes for the Noorle management API, JSON-RPC error codes for MCP and A2A gateways, and the rate limits and account quotas Noorle enforces

Noorle has three request surfaces, and they do not share an error format. The
management API returns a JSON error body with an HTTP status. The MCP and A2A
gateways speak JSON-RPC 2.0 and carry failures in the response envelope.

## Management API errors

Errors from `api.noorle.com` serialize as a flat object — there is no nested
`error` wrapper.

```json theme={null}
{
  "timestamp": "2026-08-04T10:30:45.123456Z",
  "category": "Authorization",
  "message": "Token is not valid"
}
```

| Field       | Always present         | Meaning                                           |
| ----------- | ---------------------- | ------------------------------------------------- |
| `timestamp` | yes                    | When the error was generated (RFC 3339, UTC)      |
| `category`  | yes                    | The error class, which determines the HTTP status |
| `message`   | on most errors         | Human-readable description                        |
| `code`      | rarely                 | Reserved; omitted when unset                      |
| `details`   | validation errors only | Per-field validation failures                     |

Validation errors carry `details` instead of `message`, keyed by field name.

<Note>
  **404 responses have no body.** When the category is `NotFound`, the API
  returns the bare status with no JSON payload. Do not parse the body of a 404.
</Note>

### Category to status code

Every management API status comes from one of these eleven categories.

| `category`           | HTTP status                |
| -------------------- | -------------------------- |
| `Malformed`          | 400 Bad Request            |
| `Authentication`     | 401 Unauthorized           |
| `Authorization`      | 403 Forbidden              |
| `NotFound`           | 404 Not Found (empty body) |
| `Timeout`            | 408 Request Timeout        |
| `Conflict`           | 409 Conflict               |
| `Validation`         | 422 Unprocessable Entity   |
| `RateLimit`          | 429 Too Many Requests      |
| `Internal`           | 500 Internal Server Error  |
| `PartnerSystems`     | 502 Bad Gateway            |
| `ServiceUnavailable` | 503 Service Unavailable    |

Successful responses return `200 OK`. The only endpoint that returns `204 No
Content` is the internal billing cache-invalidation callback. Nothing on the
public management surface returns `201 Created`.

`413 Payload Too Large` is returned by the plugin upload endpoint when the
request body exceeds its limit — see [Upload Plugin](/docs/reference/rest/plugins-upload).

### What each category means for you

<AccordionGroup>
  <Accordion title="403 Forbidden — credentials and permissions">
    The management API returns **403 for credential problems as well as
    permission problems**: a missing `Authorization` header, a malformed token,
    an expired token, a revoked API key, and an insufficiently privileged
    caller all produce `category: "Authorization"`.

    A 403 therefore does not tell you whether to re-authenticate or to ask for
    access. Check the `message` field, which distinguishes them
    (`"Authorization header is missing"` vs. `"Account access denied"`).

    Across the platform, an authority denial is 403 and a resource that exists
    outside your account's tenancy floor is 404 — a denied caller cannot probe
    for the existence of another account's resources.
  </Accordion>

  <Accordion title="404 Not Found — no body">
    Returned when the resource does not exist, or exists outside your account.
    The response body is empty; there is nothing to parse.
  </Accordion>

  <Accordion title="422 Unprocessable Entity — validation">
    Field-level validation failed. `message` is absent; read `details`, which
    maps each rejected field to its validation messages.
  </Accordion>

  <Accordion title="503 Service Unavailable — dependency down">
    A dependency is temporarily unavailable. This is deliberately distinct from
    500 so clients can retry without treating it as an application bug.
  </Accordion>
</AccordionGroup>

## JSON-RPC errors (MCP and A2A)

Both gateway surfaces return JSON-RPC 2.0 errors. The A2A gateway returns
**HTTP 200 with the error in the envelope** — the HTTP status is not the signal.

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}
```

### Standard codes

| Code   | Meaning                    |
| ------ | -------------------------- |
| -32700 | Parse error — invalid JSON |
| -32600 | Invalid request            |
| -32601 | Method not found           |
| -32602 | Invalid params             |
| -32603 | Internal error             |

### Noorle server-defined codes

Both sit in the JSON-RPC server-error range (-32000 to -32099).

| Code       | Surface       | Meaning                                                                                             |
| ---------- | ------------- | --------------------------------------------------------------------------------------------------- |
| **-32029** | MCP gateway   | Rate limit exhausted. Retryable — back off and retry.                                               |
| **-32050** | Agent runtime | An autonomy gate paused the call and is waiting on a human decision. Not an error to retry blindly. |

A rate-limit error carries a structured `data` payload:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32029,
    "message": "Rate limit exceeded for gateway ...",
    "data": {
      "code": "RATE_LIMIT_EXCEEDED",
      "limit": 600,
      "window_secs": 60
    }
  }
}
```

`-32029` is deliberately not `-32600 Invalid request`: many clients treat
`Invalid request` as non-retryable, and a rate limit is retryable by contract.

## Limits and quotas

Noorle enforces two kinds of limit, and it helps to keep them apart:

* **Request-rate limits** on a handful of specific endpoints, listed below.
* **Economic and object-count quotas** per account — how many skills you can
  keep, how many times your triggers may fire in a day, how much attachment
  storage you consume in a month.

**There is no account-wide request-rate quota.** Noorle does not meter total
requests per account across the platform. The rate limits that exist are
per-endpoint and are named individually here; everything else is bounded by the
account quotas instead.

<Warning>
  **No response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, or
  `X-RateLimit-Reset`.** Noorle does not emit those headers on any surface. Do
  not key backoff logic on them. The one rate-limit response header Noorle emits
  is `Retry-After: 60`, on the MCP gateway handshake 429.
</Warning>

### Request-rate limits

Fixed-window counters — no burst allowance, no token bucket.

| Limit                                | Value            | Window      | Keyed on                                                      | Exceeded response                   |
| ------------------------------------ | ---------------- | ----------- | ------------------------------------------------------------- | ----------------------------------- |
| MCP gateway requests                 | **600 / minute** | 60 s, fixed | Gateway × caller principal (anonymous callers key on peer IP) | JSON-RPC `-32029`                   |
| MCP gateway handshake (`initialize`) | **60 / minute**  | 60 s, fixed | Gateway × caller principal (anonymous callers key on peer IP) | HTTP **429** with `Retry-After: 60` |
| MCP gateway token endpoint           | **10 / minute**  | 60 s, fixed | Client IP × gateway host                                      | HTTP **429**                        |
| MCP gateway refresh-token grant      | **5 / minute**   | 60 s, fixed | Client IP × `client_id`                                       | HTTP **429**                        |
| Outbound SMS                         | **100 / hour**   | 60 min      | Sending phone number                                          | Message not sent                    |

Two properties worth designing around:

* **`tools/call` and `tools/list` share one budget.** The 600/minute limit
  covers the authenticated request path, not each method separately.
  `initialize` and `ping` sit outside it — that is why the handshake has its own
  limiter.
* **The MCP limiters favor availability.** If the counter's backing store is
  unreachable, requests are allowed through rather than rejected.

The last two rows are worth reading carefully. The token-endpoint limits belong
to **each MCP gateway's own OAuth server** — they are keyed on the gateway host
and apply at `mcp-{handle}.noorle.com/oauth/token`. The management API's device
flow at `api.noorle.com/oauth/token` is **not** rate limited.

The agent (A2A) gateway has **no rate limiting**.

### Account quotas

These are the ceilings that actually bound a busy account.

| Quota                   | Limit              | Scope   | Resets         |
| ----------------------- | ------------------ | ------- | -------------- |
| Trigger fires           | **500 / day**      | Account | Midnight UTC   |
| Trigger fires           | **100 / day**      | Agent   | Midnight UTC   |
| Attachments             | **10,000 / month** | Account | Calendar month |
| Attachment storage      | **1 GiB / month**  | Account | Calendar month |
| Attachments per message | **10**             | Message | —              |
| Attachment file size    | **20 MiB**         | File    | —              |
| Skills                  | **1,000**          | Account | —              |
| Workflows               | **1,000**          | Account | —              |

A trigger firing consumes both budgets at once — the agent's and the account's —
and is rejected if either is exhausted, so one busy agent cannot drain the
account's whole daily allowance on its own.

Attachments beyond the per-message cap, or arriving after a monthly ceiling is
reached, are not processed; the message itself still goes through.

Beyond these, several surfaces enforce their own size and time caps — request
body size, plugin archive size, tool execution timeouts. Those are documented
with the endpoints that enforce them.

## Handling failures

Retry on `429`, `500`, `502`, `503`, `504`, and JSON-RPC `-32029`. Do not retry
`400`, `403`, `404`, or `422` — the request will fail identically.

```python theme={null}
import time
import random
import requests

RETRYABLE = {429, 500, 502, 503, 504}

def call_with_retry(url, headers, max_attempts=4):
    for attempt in range(max_attempts):
        resp = requests.get(url, headers=headers, timeout=30)
        if resp.status_code not in RETRYABLE:
            return resp
        if attempt == max_attempts - 1:
            resp.raise_for_status()
        # Full jitter. No rate-limit headers exist to read a reset time from.
        time.sleep(random.uniform(0, 2 ** attempt))
```

For the MCP gateway, inspect the JSON-RPC error code rather than the HTTP
status — a rate-limited `tools/call` arrives as HTTP 200 carrying `-32029`.

```typescript theme={null}
const body = await response.json();
if (body.error?.code === -32029) {
  const { limit, window_secs } = body.error.data ?? {};
  // Back off for at least window_secs before retrying.
}
```

## Next steps

* [Authentication](/docs/reference/authentication)
* [MCP Protocol](/docs/reference/mcp/overview)
* [A2A Protocol](/docs/reference/a2a/overview)
