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
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" },
});{
"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
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);{
"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
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY);
const scope = await api.scopes.get(scopeId);{
"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
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
});{
"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
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);Error while actors are still assigned: 409 Conflict
{
"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
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);
}[
{
"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 - create your first scope
- Concepts - understand how scopes work under the hood
- Multi-Tenancy Patterns - real-world usage patterns