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

FieldTypeRequiredDescription
slugstringYesTopic 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.
namestringYesHuman-readable display name. Max 255 characters.
descriptionstringNoFree-text description. Max 500 characters.
metadataobjectNoArbitrary 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

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed).
limitnumber10Results per page (max 100).
slugstringExact match on slug.
namestringCase-insensitive substring match on name.
isActive"true" | "false"Filter by active state. Passed as a string because it arrives in the query string.
orderBy[]stringSecondary 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

ParameterTypeDescription
scopeIdUUIDThe 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.

FieldTypeRequiredDescription
namestringNoUpdated display name. Max 255 characters.
descriptionstringNoUpdated description. Max 500 characters.
metadataobjectNoReplaces the stored metadata object.
isActivebooleanNoSet 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

FieldTypeDescription
accessScopeIdUUIDUnique identifier for the scope.
projectIdUUIDThe project the scope belongs to.
slugstringTopic segment for this scope. Immutable.
namestringHuman-readable display name.
descriptionstring | nullFree-text description.
metadataobject | nullArbitrary JSON object.
isActivebooleanWhether the scope is active. An inactive scope denies its actors.
createdAtISO 8601Timestamp when the scope was created.
updatedAtISO 8601Timestamp of the last update.

Error Responses

StatusDescription
400Invalid 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.
401Missing or invalid API key.
403The API key is not project-scoped.
404Scope not found in this project.
409On create: the slug already exists within this project. On delete: actors are still assigned to the scope.

Next Steps