Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

API reference

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
1
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
1
2
3
4
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "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
1
2
3
4
5
6
7
8
{
  "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.
Updated 2026-05-18 15:01:30 View source (.md) rev 12