---
summary: "Every ScaiLink endpoint \u2014 auth discovery, the desktop WebSocket, sessions,\
  \ capabilities, invocations, consent, audit, cloud MCP registry."
title: API reference
path: reference/api
status: published
---

The REST surface is mounted at `/v1/modules/scailink/` and authenticates with the standard ScaiGrid bearer token. The desktop WebSocket lives at `/v1/scailink/ws` and authenticates with a user JWT in the `Authorization` header. Responses use ScaiGrid's standard envelope: `{ "data": ... }` for success, `{ "error": ... }` for failures.

## Auth discovery

### `GET /v1/scailink/auth/discover`

Public, unauthenticated. The first endpoint a desktop client calls. Returns OAuth metadata so the client knows where to send the user for sign-in.

```bash
curl "$SCAIGRID_HOST/v1/scailink/auth/discover"
```

Response fields: `issuer`, `authorization_endpoint`, `token_endpoint`, `device_authorization_endpoint`, `userinfo_endpoint`, `jwks_uri`, `scopes_supported`, `grant_types_supported`, `client_id`, `audience`, `gateway_ws`.

## Desktop WebSocket

### `WS /v1/scailink/ws`

Authenticated WebSocket carrying JSON-RPC 2.0 frames. The first frame from the client must be `scailink/session_init`; anything else closes the connection with code 4002.

Client-to-server methods:

| Method | Purpose |
|---|---|
| `scailink/session_init` | Handshake with device info, platform, capability catalog, audit settings. |
| `scailink/heartbeat` | Keepalive; sent every `heartbeat_interval_ms` (default 30000). |
| `scailink/catalog_update` | Push add/remove deltas to the live catalog without reconnecting. |
| `scailink/consent_response` | Answer an outstanding consent prompt (`approved` or `denied`). |
| `scailink/session_terminate` | Clean shutdown signal. |

Server-to-client methods:

| Method | Purpose |
|---|---|
| `scailink/session_init_ack` | Response to the handshake; includes `session_id`, `heartbeat_interval_ms`, `grace_period_ms`. |
| `scailink/tool_invoke` | Ask the client to run a tool; reply with the matching `id`. |
| `scailink/resource_read` | Ask the client to read a resource. |
| `scailink/prompt_get` | Ask the client to render a prompt. |
| `scailink/consent_request` | Ask the user to approve or deny a pending invocation. |
| `scailink/policy_update` | Push an updated consent policy mid-session. |

Custom JSON-RPC error codes: `-32000 TOOL_ERROR`, `-32001 CONSENT_DENIED`, `-32002 CONSENT_TIMEOUT`, `-32003 SCOPE_BLOCKED`, `-32004 QUEUE_FULL`, `-32005 TIMEOUT`, `-32006 CLIENT_DISCONNECTED`.

## Sessions

### `GET /sessions`

List the caller's own active ScaiLink sessions across all their devices.

### `GET /sessions/all`

List all active sessions in the caller's tenant. Requires `scailink:manage`.

### `GET /users/{user_id}/sessions`

List active sessions for a specific user. Cross-user reads require same-tenant access; super admin bypasses tenant isolation. `user_id` accepts either a ScaiGrid UUID or a ScaiKey `usr_...` id.

## Capabilities

### `GET /capabilities`

Aggregated capability catalog for the caller — tools, resources, and prompts across all of the caller's connected devices.

### `GET /users/{user_id}/capabilities`

Same shape, for a specific user. Tenant-bounded unless the caller is super admin.

### `GET /device/{device_id}`

Capability catalog for a single device. Useful when a user has multiple machines connected and you want to invoke against one specifically.

## Invocations

All four below require `scailink:invoke`.

### `POST /users/{user_id}/tools/{tool_name}/invoke`

Invoke a tool on the user's connected device.

| Body field | Notes |
|---|---|
| `arguments` | Tool-specific input. |
| `conversation_id` | Optional; threaded through to the audit log and consent reuse. |
| `timeout` | Seconds. Default 30. |

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scailink/users/$USER_ID/tools/filesystem.read_file/invoke" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"arguments": {"path": "/Users/alice/notes.md"}}'
```

Returns `{ "data": { "result": ..., "error": null, "duration_ms": 145, "device_id": "..." } }`.

### `POST /users/{user_id}/resources/{resource_uri:path}/read`

Read a resource (e.g. a file URI) from the user's device. The URI is in the path; encode `/` characters appropriately.

### `POST /users/{user_id}/prompts/{prompt_name}/get`

Retrieve a prompt from the user's device.

| Body field | Notes |
|---|---|
| `arguments` | Prompt arguments per the MCP `prompts/get` shape. |
| `timeout` | Seconds. Default 15. |

### `POST /consent/{consent_id}/resolve`

Resolve a pending consent request out of band — when the user approves or denies through a UI separate from the WebSocket itself.

| Body field | Notes |
|---|---|
| `approved` | Boolean. |
| `comment` | Free-form audit annotation. |

## Audit

### `GET /audit`

Tenant-wide audit log. Requires `scailink:audit`.

Query parameters: `limit` (1-200, default 50), `offset` (default 0).

```json
{
  "data": [
    {
      "id": "evt_...",
      "action": "tool_invoke",
      "target_name": "filesystem.read_file",
      "status": "approved",
      "duration_ms": 145,
      "device_id": "...",
      "detail_level": "metadata",
      "created_at": "2026-05-17T12:00:00Z"
    }
  ]
}
```

### `GET /users/{user_id}/audit`

Per-user audit log. Same shape, scoped to one user. `scailink:audit` required.

## Cloud MCP registry

All endpoints below are under `/v1/modules/scailink/remote-servers`. Every call requires `scailink:remote.use` as the precondition; mutating calls additionally require `manage_own` (for personal servers) or `manage_tenant` (for tenant-shared).

### `GET /remote-servers`

List servers visible to the caller — their own personal servers plus any tenant-shared servers in the caller's tenant.

```json
{
  "data": {
    "items": [
      {
        "id": "...",
        "tenant_id": "...",
        "owner_user_id": "...",
        "is_tenant_shared": false,
        "name": "Acme Notion MCP",
        "slug": "acme-notion-mcp-a1b2c3",
        "endpoint_url": "https://...",
        "transport": "streamable_http",
        "auth_type": "bearer",
        "forward_user_id": false,
        "consent_tier": "auto",
        "status": "active",
        "last_health_check_at": "...",
        "last_health_status": "ok",
        "consecutive_failures": 0,
        "created_at": "..."
      }
    ]
  }
}
```

### `GET /remote-servers/{id}`

Detail view. Same fields as list plus `capabilities` (discovered tools/resources/prompts), `credential_fields` (the field names you can rotate), and `credential_oldest_days`.

### `POST /remote-servers`

Register a new server. Returns `201 Created`.

| Field | Required | Notes |
|---|---|---|
| `name` | yes | 1-120 chars; drives the slug. |
| `endpoint_url` | yes | 1-500 chars. |
| `auth_type` | yes | `none`, `bearer`, or `api_key_header`. |
| `credentials` | conditional | Flat `{field_name: value}`. Required unless `auth_type=none`. Field names map to outbound HTTP header names verbatim. |
| `description` | no | Up to 1000 chars. |
| `is_tenant_shared` | no | Default false. True needs `scailink:remote.manage_tenant`. |
| `forward_user_id` | no | Default false. True adds `X-ScaiGrid-User`. |
| `consent_tier` | no | `auto` (default), `confirm`, or `locked`. |
| `transport` | no | `streamable_http` (default) or `sse`. |

Registration triggers an immediate discovery. The row is committed even if discovery fails (`status='error'`).

### `DELETE /remote-servers/{id}`

Remove the server. Cascades through `mod_scailink_remote_credential` and `mod_scailink_remote_capability`. Personal servers need `manage_own`; tenant-shared need `manage_tenant`.

### `POST /remote-servers/{id}/refresh`

Force re-discovery. Updates capability rows and health. Gated on `scailink:remote.use` (same perm as listing).

### `PUT /remote-servers/{id}/credentials/{field}`

Rotate one credential field. Idempotent — overwrites the existing ciphertext and DEK.

Body: `{ "value": "..." }`.

Personal servers need `manage_own`; tenant-shared need `manage_tenant`.

## Errors

All endpoints return ScaiGrid's standard error envelope:

```json
{
  "error": {
    "code": "SCAILINK_REMOTE_SERVER_NOT_FOUND",
    "message": "Remote MCP server not found",
    "details": { "server_id": "..." }
  },
  "meta": { "request_id": "req_..." }
}
```

ScaiLink-specific codes:

| Code | Status | Meaning |
|---|---|---|
| `SCAILINK_REMOTE_SERVER_NOT_FOUND` | 404 | Server id not visible to the caller. |
| `SCAILINK_REMOTE_LIMIT_EXCEEDED` | 429 | Tenant hit the registration cap (100). |
| `SCAILINK_REMOTE_INVALID_CONFIG` | 400 | Registration body didn't validate (bad `auth_type`, `transport`, `consent_tier`). |
| `SCAILINK_REGISTRY_DISABLED` | 503 | Platform KEK is unset; cloud registry feature is off in this deployment. |

WebSocket close codes you may see from the desktop bridge:

| Code | Meaning |
|---|---|
| 4001 | Authentication failed (bad or missing JWT). |
| 4002 | First frame was not `session_init`, or `session_init` params didn't validate. |
| 4000 | Generic close from the gateway side. |
