---
title: Error Reference
description: Complete reference for NoLag REST API and WebSocket errors, status codes, and error handling.
---

# Error Reference

Complete reference for errors returned by the NoLag REST API and WebSocket connections.

## REST API Errors

### Error Format

REST error responses come in two shapes, depending on which layer raised the error. Read `message` in both cases.

**Application errors** (unknown resource, duplicate slug, invalid identifier, bad credentials) carry a unique `id` you can quote to support:

```json [JSON]
{
  "id": "uuid",
  "message": "Human-readable error description",
  "timestamp": "2024-01-15T12:00:00Z"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (UUID) | Unique identifier for this error instance. Include this when contacting support. |
| `message` | string | Human-readable description of what went wrong. |
| `timestamp` | string (ISO 8601) | When the error occurred. |

**Framework errors** (a key that is not project-scoped, an app outside your project, a plan limit) use the standard NestJS shape, with no `id` or `timestamp`:

```json [JSON]
{
  "statusCode": 403,
  "message": "API key must be project-scoped to access this endpoint",
  "error": "Forbidden"
}
```

**Validation errors** are a third variant of the framework shape: a `400` with `"message": "Validation failed"` and an `errors` array, one entry per failed constraint (`path`, `value`, `message`). Unknown body fields fail validation rather than being ignored.

### HTTP Status Codes

| Status | Name | When |
| --- | --- | --- |
| `400` | Bad Request | Validation failed, invalid UUID, unknown body field, room cap reached, static room deletion, scope from another project |
| `401` | Unauthorized | Missing or invalid API key/token |
| `403` | Forbidden | API key is not project-scoped, plan app limit reached, `rooms/ensure` on an app without `config.autoProvisionRooms` |
| `404` | Not Found | Resource does not exist, has been deleted, or belongs to another project |
| `409` | Conflict | Duplicate slug, duplicate room grant, deleting a scope that still has actors |

### Response Examples

#### 400 Bad Request

Returned when the request fails validation or contains malformed data.

```json [JSON]
{
  "id": "01939f83-8b57-7c3e-a456-426614174001",
  "message": "Invalid appId UUID format",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```
```json [Validation]
{
  "message": "Validation failed",
  "errors": [
    {
      "path": "actorType",
      "value": "server",
      "message": "actorType must be one of the following values: device, service, session, user, agent, orchestrator, observer"
    }
  ]
}
```

Other `400` messages you will meet: `Invalid actorId: must be a UUID or public keyId (at_...)`, `App <appId> has reached its room cap (1000)`, `Cannot delete static rooms created from Blueprint. Disable them instead.`, `Access scope <id> does not belong to this project`, `Either actorTokenId or actorType must be provided.`

#### 401 Unauthorized

Returned when the API key is missing, malformed, or no longer valid.

```json [JSON]
{
  "id": "01939f83-8b57-7c3e-a456-426614174002",
  "message": "Invalid or expired API key",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```

Possible `message` values: `Authorization header not provided`, `Invalid or expired API key`, `Token type not allowed for this route` (a user session token was sent to an API-key route).

#### 403 Forbidden

Returned when the credentials are valid but lack permission to access the resource.

```json [JSON]
{
  "statusCode": 403,
  "message": "API key must be project-scoped to access this endpoint",
  "error": "Forbidden"
}
```

Other `403` messages: `App limit reached. Please upgrade your subscription to create more apps.` and, for `POST /apps/{appId}/rooms/ensure`, `App <appId> does not allow dynamic room provisioning (set config.autoProvisionRooms=true to enable). Rooms must be created explicitly; unknown rooms are rejected at the broker.`

#### 404 Not Found

Returned when the requested resource does not exist, has been deleted, or belongs to a different project than the API key.

```json [App (framework shape)]
{
  "statusCode": 404,
  "message": "App 01939f83-8b57-7c3e-a456-426614174000 not found in project 019e8d1e-a59b-748b-9f69-dd03a91400f8",
  "error": "Not Found"
}
```
```json [Room, actor, scope, grant]
{
  "id": "01939f83-8b57-7c3e-a456-426614174004",
  "message": "Room 01939f83-8b57-7c3e-a456-426614174003 not found",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```

Other `404` messages: `Actor token <id> not found`, `Actor token <id> not found in this project` (grant to an actor from another project), `Access scope <id> not found`, `Room grant <id> not found`.

#### 409 Conflict

Returned when a create or update would violate a uniqueness constraint.

```json [JSON]
{
  "id": "01939f83-8b57-7c3e-a456-426614174005",
  "message": "Room with slug \"general\" already exists",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```

Other `409` messages: `App with slug "<slug>" already exists in this project` (framework shape), `A scope with slug "<slug>" already exists in this project`, `That actor already has a grant on this room. Update it instead.`, `Cannot delete scope: <n> actor(s) are still assigned to it. Unscope them first.`

## WebSocket Errors

### Error Format

WebSocket errors are delivered as `error` frames on the open connection (MessagePack encoded, shown here as JSON):

```json [JSON]
{
  "type": "error",
  "error": "unknown_topic",
  "code": 42940,
  "topic": "my-app-a3f9/general/messages",
  "hint": "room is not configured. Provision it via the control-plane rooms API before use."
}
```

| Field | Type | Description |
| --- | --- | --- |
| `type` | string | Always `"error"`. |
| `error` | string | Machine-readable error name. |
| `code` | number | Numeric error code. Present on rate, quota, size and unknown-topic errors. |
| `topic` | string | Optional. Included when the error relates to a specific topic. |
| `hint` | string | Optional. Human-readable remediation, currently sent with `unknown_topic`. |
| `msgRef` | string | Optional. Echoes the `msgRef` of the `publish` frame that failed, so a client can settle the matching publish callback. |
| `lobbyId` | string | Optional. Included instead of `topic` on lobby errors. |
| `maxSizeBytes` | number | Optional. Included with `message_too_large`. |

`code`, `hint` and `msgRef` are protocol v2 fields. Send `"protocolVersion": 2` in the `auth` frame (every current SDK does) to receive them; a v1 connection gets the legacy `not_authorized` frame for a denied topic and never receives `published` acks.

### Error Codes

| Code | Error | Description |
| --- | --- | --- |
| `42910` | `rate_limit_exceeded` | More than 50 publishes per second on this connection. |
| `42920` | `monthly_quota_exceeded` | Project has exceeded its monthly message quota. |
| `42930` | `message_too_large` | Payload exceeds the 900 KB ceiling (921,600 bytes, the same on every plan). The frame includes `maxSizeBytes`. |
| `42940` | `unknown_topic` | Subscribe or publish to a room that does not exist or that this actor may not access. Rooms are never created implicitly; create them via the [REST API](/docs/api-reference#rooms) first. Carries `hint`. |
| - | `not_authorized` | Lobby access denied (`lobbyId` set). Also the v1 fallback for a denied topic on connections that did not send `protocolVersion: 2`. |
| - | `not_subscribed` | `setFilters` on a topic this connection has not subscribed to. |
| - | `invalid_filter_chars (/, #, +, \| not allowed)` | A filter contains `/`, `#`, `+` or `\|`, is an empty string, or an AND-group has fewer than two entries. The wire string includes the parenthetical. |
| - | `too_many_filters (max 100)` | More than 100 filters specified for a topic. The wire string includes the parenthetical. |
| - | `not_authenticated` | Any frame other than `auth` sent before authentication completed. |

### Close Codes

The server may close the WebSocket connection with a specific close code:

| Code | Reason | Description |
| --- | --- | --- |
| `4001` | `token_revoked`, `token_expired`, `token_not_found`, `project_not_found`, `scope_inactive` | Periodic revalidation (about every 10 minutes) rejected the actor. The reason string is the close reason. No `disconnect` frame precedes this close. |
| `4002` | `connection_limit_reached` | The organization went over its concurrent connection limit after a plan change, detected at revalidation. At connect time the same condition is reported as an `auth` failure frame, not a close. |
| `4003` | `token_expired` | A client token (JWT) passed its `exp`, detected at the next heartbeat. Preceded by a `disconnect` frame. |

A silent connection is also closed by the server's 60-second idle timeout; send a heartbeat (empty binary frame) at least that often.

### Server-Initiated Disconnect

Before a `4003` close the server sends a `disconnect` frame. This is the only reason the frame carries today:

```json [JSON]
{
  "type": "disconnect",
  "reason": "token_expired"
}
```

Renew the client token with a `reauth` frame before it expires to avoid this; see [WebSocket API](/docs/api-reference/websocket#reauth).

### Authentication Errors

If authentication fails, the server responds with an `auth` frame indicating the failure and leaves the socket open; the client may retry with another token or close.

```json [JSON]
{
  "type": "auth",
  "success": false,
  "error": "access_denied"
}
```

Possible `error` values:

- `"access_denied"`: the control plane rejected the token. Covers an unknown, disabled, expired or cross-project actor, a client token with a bad signature, and an actor whose scope is inactive, deleted or belongs to another project (`scope_inactive` on the control plane; the broker does not forward the specific reason at connect).
- `"authentication_failed"`: the control plane returned an unexpected status.
- `"connection_failed"`: the broker could not reach the control plane.
- `"token_expired"`: a client token (JWT) whose `exp` has already passed.
- `"connection_limit_reached"`: the organization's concurrent connection limit is full.
- `"broker_unavailable"`: the broker's internal message bus is unreachable.

### Re-authentication Errors

A `reauth` frame (used to renew a client token on the live connection) is answered with `{ "type": "reauth", "success": false, "error": ... }` when it fails. The connection stays open under its current authentication until that expires.

- `"actor_mismatch"`: the new token resolves to a different actor than the connection was opened with.
- `"token_expired"`: the new token has already expired.
- Any of the authentication error strings above when the control plane rejects the token.

## Next Steps

- [REST API Reference](/docs/api-reference)
- [WebSocket API Reference](/docs/api-reference/websocket)
- [Wire Protocol](/docs/protocol)
