---
title: Access Scopes - API Reference
description: Complete API reference for Access Scopes endpoints. Create, list, get, update, and delete scopes. List actors assigned to a scope.
---

# Access Scopes - API Reference

Complete reference for all Access Scope REST API endpoints.

All endpoints use the base path:\
`https://api.nolag.app/v1/scopes`

Authentication is required via `Authorization: Bearer <api-key>` header. Use a project-scoped API key (`nlg_live_...`). The project context is determined by the API key; a key that is not project-scoped gets `403 Forbidden` on every endpoint here.

The same operations are available from the SDK REST clients: `api.scopes.list/get/create/update/delete/listActors/addActor/removeActor` on `NoLagApi` in `@nolag/js-sdk`, `api.scopes.list/get/create/update/delete/list_actors/add_actor/remove_actor` on `NoLagApi` in the Python SDK, and `api.Scopes.List/Get/Create/Update/Delete/ListActors/AddActor/RemoveActor` on `nolag.NewAPI(key)` in the Go SDK.

## Create Scope

`POST /v1/scopes`

### Request Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | Yes | Topic segment for this scope. Must match `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (lowercase letters, digits, hyphens; no leading or trailing hyphen), max 100 characters. Immutable after creation. |
| `name` | string | Yes | Human-readable display name. Max 255 characters. |
| `description` | string | No | Free-text description. Max 500 characters. |
| `metadata` | object | No | Arbitrary JSON object stored with the scope, for example `{ "tenantId": "tenant-123" }`. |

### Response: 201 Created

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

const scope = await api.scopes.create({
  slug: "client-acme",
  name: "Acme Corporation",
  description: "Isolation scope for the Acme tenant",
  metadata: { tenantId: "tenant-123" },
});
```

```bash [Terminal]
curl -X POST https://api.nolag.app/v1/scopes \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{
        "slug": "client-acme",
        "name": "Acme Corporation",
        "description": "Isolation scope for the Acme tenant",
        "metadata": { "tenantId": "tenant-123" }
      }'
```

```json
{
  "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
  "projectId": "01939f83-8b57-7c3e-a456-426614174001",
  "slug": "client-acme",
  "name": "Acme Corporation",
  "description": "Isolation scope for the Acme tenant",
  "metadata": { "tenantId": "tenant-123" },
  "isActive": true,
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-05-20T10:00:00.000Z"
}
```

## List Scopes

`GET /v1/scopes`

### Query Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `page` | number | 1 | Page number (1-indexed). |
| `limit` | number | 10 | Results per page (max 100). |
| `slug` | string | | Exact match on `slug`. |
| `name` | string | | Case-insensitive substring match on `name`. |
| `isActive` | `"true"` \| `"false"` | | Filter by active state. Passed as a string because it arrives in the query string. |
| `orderBy[]` | string[] | | Secondary sort as `field:ASC` or `field:DESC` pairs, for example `orderBy[]=slug:ASC`. Results are always ordered by `createdAt` descending first; `orderBy` only breaks ties. |

### Response: 200 OK

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

const page = await api.scopes.list({ page: 1, limit: 10 });
for (const scope of page.data) {
  console.log(scope.slug, scope.isActive);
}
console.log(page.pagination.total, page.pagination.pageCount);
```

```bash [Terminal]
curl "https://api.nolag.app/v1/scopes?page=1&limit=10&isActive=true" \
  -H "Authorization: Bearer nlg_live_xxx.secret"
```

```json
{
  "data": [
    {
      "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
      "projectId": "01939f83-8b57-7c3e-a456-426614174001",
      "slug": "client-beta",
      "name": "Beta Corp",
      "description": null,
      "metadata": null,
      "isActive": true,
      "createdAt": "2026-05-21T09:00:00.000Z",
      "updatedAt": "2026-05-21T09:00:00.000Z"
    },
    {
      "accessScopeId": "01939f83-8b57-7c3e-a456-426614174003",
      "projectId": "01939f83-8b57-7c3e-a456-426614174001",
      "slug": "client-acme",
      "name": "Acme Corporation",
      "description": "Isolation scope for the Acme tenant",
      "metadata": { "tenantId": "tenant-123" },
      "isActive": true,
      "createdAt": "2026-05-20T10:00:00.000Z",
      "updatedAt": "2026-05-20T10:00:00.000Z"
    }
  ],
  "pagination": { "total": 2, "page": 1, "pageCount": 1 }
}
```

## Get Scope

`GET /v1/scopes/:scopeId`

### Path Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `scopeId` | UUID | The Access Scope ID. A value that is not a UUID returns `400`. |

### Response: 200 OK

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

const scope = await api.scopes.get(scopeId);
```

```bash [Terminal]
curl https://api.nolag.app/v1/scopes/01939f83-8b57-7c3e-a456-426614174002 \
  -H "Authorization: Bearer nlg_live_xxx.secret"
```

```json
{
  "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
  "projectId": "01939f83-8b57-7c3e-a456-426614174001",
  "slug": "client-acme",
  "name": "Acme Corporation",
  "description": "Isolation scope for the Acme tenant",
  "metadata": { "tenantId": "tenant-123" },
  "isActive": true,
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-05-20T10:00:00.000Z"
}
```

## Update Scope

`PATCH /v1/scopes/:scopeId`

### Request Body

All fields are optional; only the fields you send are changed. The `slug` cannot be updated.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Updated display name. Max 255 characters. |
| `description` | string | No | Updated description. Max 500 characters. |
| `metadata` | object | No | Replaces the stored metadata object. |
| `isActive` | boolean | No | Set to `false` to deactivate the scope. Actors assigned to an inactive scope are denied: refused at connect with `scope_inactive`, and disconnected at their next revalidation if already connected. They are not unscoped. |

### Response: 200 OK

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

const scope = await api.scopes.update(scopeId, {
  name: "Acme Corp (Renamed)",
  // slug cannot be updated after creation
});
```

```bash [Terminal]
curl -X PATCH https://api.nolag.app/v1/scopes/01939f83-8b57-7c3e-a456-426614174002 \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Corp (Renamed)"}'
```

```json
{
  "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
  "projectId": "01939f83-8b57-7c3e-a456-426614174001",
  "slug": "client-acme",
  "name": "Acme Corp (Renamed)",
  "description": "Isolation scope for the Acme tenant",
  "metadata": { "tenantId": "tenant-123" },
  "isActive": true,
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-05-20T12:00:00.000Z"
}
```

## Delete Scope

`DELETE /v1/scopes/:scopeId`

Soft-deletes the scope. The request is refused with `409 Conflict` while any actor is still assigned to the scope, because deleting it out from under them would silently widen their access to the project-wide namespace. Unscope the actors first with `PATCH /v1/actors/:actorTokenId { "accessScopeId": null }` (or `api.scopes.removeActor(actorTokenId)`), then delete.

### Response: 204 No Content

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

// Unscope every actor still assigned, then delete
const actors = await api.scopes.listActors(scopeId);
for (const actor of actors) {
  await api.scopes.removeActor(actor.actorTokenId);
}
await api.scopes.delete(scopeId);
```

```bash [Terminal]
# Unscope an assigned actor
curl -X PATCH https://api.nolag.app/v1/actors/{actorTokenId} \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"accessScopeId":null}'

# Then delete the scope
curl -X DELETE https://api.nolag.app/v1/scopes/01939f83-8b57-7c3e-a456-426614174002 \
  -H "Authorization: Bearer nlg_live_xxx.secret"
```

Error while actors are still assigned: `409 Conflict`

```json
{
  "id": "6f1c2a9e-3b4d-4e5f-8a6b-7c8d9e0f1a2b",
  "message": "Cannot delete scope: 2 actor(s) are still assigned to it. Unscope them first.",
  "timestamp": "2026-05-22T09:30:00.000Z"
}
```

## List Actors in Scope

`GET /v1/scopes/:scopeId/actors`

Returns every actor currently assigned to this scope as a plain array, newest first (ordered by `createdAt` descending). This endpoint takes no query parameters and is not paginated. A `scopeId` from another project returns `404`.

### Response: 200 OK

```typescript [TypeScript]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY);

const actors = await api.scopes.listActors(scopeId);
for (const actor of actors) {
  console.log(actor.actorTokenId, actor.name, actor.actorType);
}
```

```bash [Terminal]
curl https://api.nolag.app/v1/scopes/01939f83-8b57-7c3e-a456-426614174002/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret"
```

```json
[
  {
    "actorTokenId": "01939f83-8b57-7c3e-a456-426614174011",
    "projectId": "01939f83-8b57-7c3e-a456-426614174001",
    "keyId": "at_live_9f2c1b7e4a3d",
    "name": "user-bob",
    "actorType": "user",
    "status": "active",
    "expiresAt": null,
    "lastUsedAt": "2026-05-22T08:15:00.000Z",
    "metadata": null,
    "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
    "createdAt": "2026-05-21T10:00:00.000Z"
  },
  {
    "actorTokenId": "01939f83-8b57-7c3e-a456-426614174010",
    "projectId": "01939f83-8b57-7c3e-a456-426614174001",
    "keyId": "at_live_5d8e2a1c9b4f",
    "name": "user-alice",
    "actorType": "user",
    "status": "active",
    "expiresAt": null,
    "lastUsedAt": null,
    "metadata": null,
    "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
    "createdAt": "2026-05-20T11:00:00.000Z"
  }
]
```

## Assigning Actors

Scope membership lives on the actor, not on the scope. There is no `POST /v1/scopes/:scopeId/actors`; instead set `accessScopeId` on the actor:

- `POST /v1/actors { "name", "actorType", "accessScopeId" }` creates an actor already in the scope.
- `PATCH /v1/actors/:actorTokenId { "accessScopeId": "<scope id>" }` moves an existing actor into the scope.
- `PATCH /v1/actors/:actorTokenId { "accessScopeId": null }` unscopes it.

The scope must belong to the same project as the actor; a scope from another project is refused with `400`. The change applies at the actor's next connection, or within about 10 minutes on a live connection. In `@nolag/js-sdk`, `api.scopes.addActor(scopeId, actorTokenId)` and `api.scopes.removeActor(actorTokenId)` wrap the same `PATCH`.

## Response Schemas

### AccessScope

| Field | Type | Description |
| --- | --- | --- |
| `accessScopeId` | UUID | Unique identifier for the scope. |
| `projectId` | UUID | The project the scope belongs to. |
| `slug` | string | Topic segment for this scope. Immutable. |
| `name` | string | Human-readable display name. |
| `description` | string \| null | Free-text description. |
| `metadata` | object \| null | Arbitrary JSON object. |
| `isActive` | boolean | Whether the scope is active. An inactive scope denies its actors. |
| `createdAt` | ISO 8601 | Timestamp when the scope was created. |
| `updatedAt` | ISO 8601 | Timestamp of the last update. |

### Error Responses

| Status | Description |
| --- | --- |
| `400` | Invalid request body or parameters: missing `slug` or `name`, slug outside `^[a-z0-9][a-z0-9-]*[a-z0-9]$`, field over its maximum length, `scopeId` that is not a UUID. |
| `401` | Missing or invalid API key. |
| `403` | The API key is not project-scoped. |
| `404` | Scope not found in this project. |
| `409` | On create: the slug already exists within this project. On delete: actors are still assigned to the scope. |

## Next Steps

- [Getting Started](/docs/scopes/getting-started) - create your first scope
- [Concepts](/docs/scopes/concepts) - understand how scopes work under the hood
- [Multi-Tenancy Patterns](/docs/scopes/multi-tenancy) - real-world usage patterns
