Skip to main content
Loom Core docs

Tracking

Mobile Companion API v1 (Contract Freeze)

This document defines the API contract for the Loom Companion iPhone/iPad app.

Status: v1 additive contract freeze (updated 2026-02-25). Initial M0/M1 endpoints plus read-only parity wave endpoints are implemented in internal/hud/api_mobile.go.

Tracking

Goals

  • Provide a stable mobile-facing contract without coupling the app to internal HUD handler shapes.
  • Support both connectivity modes:
    • LAN mode (trusted/local network)
    • Gateway mode (remote/zero-trust)
  • Keep v1 scope focused on monitoring + session lifecycle control.
  • Preserve read-only posture for parity-wave features (tasks/workflows/presence/memory/stream/topology/graph/reasoning).

Versioning

  • API prefix: /api/mobile/v1
  • Compatibility target: additive-only changes within v1 when possible.
  • Breaking changes require v2 or explicit migration notes.

Connectivity Modes

The same API contract is used in both modes.

ModeTypical endpointPrimary use case
LANhttps://<lan-host>:<port>/api/mobile/v1Same network, low-latency ops
Gatewayhttps://mcp.flexinfer.ai/api/mobile/v1Off-network remote operations

Notes:

  • Client profile selects mode.
  • Gateway mode must not assume LAN trust.
  • Unified gateway path split on mcp.flexinfer.ai:
    • MCP hub: /ws, /hosts, /health, /ready
    • Mobile API: /api/mobile/v1/*

Auth Model (Contract-Level)

  • Authorization: Bearer <token> required for protected endpoints.
  • See Mobile Companion Auth Bootstrap for the full auth bootstrap decision, flow diagrams, and LAN/gateway comparison.
  • Bootstrap decision (MBL-1):
    • v1 default: direct native OAuth authorization code + PKCE in an external browser/system auth session.
    • v1 fallback: device-code pairing for profiles where direct browser-mediated auth is not practical.
    • Fallback path is explicit and profile/policy selected; do not silently downgrade from OAuth+PKCE.
  • Client UX contract: connection profile setup must display the active bootstrap mode and clearly indicate when fallback mode is being used.
  • Token claims must include:
    • actor identity (sub or equivalent),
    • role/scope,
    • device/session identifier.
  • Anonymous access is limited to optional health/probe routes only.

mobile_operator Authorization Matrix (Contract View)

This section freezes the v1 endpoint allowlist for the mobile_operator role. Wave 1 parity additions are read-only only.

EndpointMethodAccessScope
/api/mobile/v1/dashboardGETallowmobile:read
/api/mobile/v1/sessionsGETallowmobile:read
/api/mobile/v1/sessions/{session_id}GETallowmobile:read
/api/mobile/v1/sessions/{session_id}/eventsGETallowmobile:read
/api/mobile/v1/tasksGETallowmobile:read
/api/mobile/v1/workflowsGETallowmobile:read
/api/mobile/v1/workflows/{workflow_id}GETallowmobile:read
/api/mobile/v1/presenceGETallowmobile:read
/api/mobile/v1/memory/statsGETallowmobile:read
/api/mobile/v1/memory/itemsGETallowmobile:read
/api/mobile/v1/streamGETallowmobile:read
/api/mobile/v1/topologyGETallowmobile:read
/api/mobile/v1/graph/statsGETallowmobile:read
/api/mobile/v1/graph/entitiesGETallowmobile:read
/api/mobile/v1/graph/pathGETallowmobile:read
/api/mobile/v1/reasoning/chainsGETallowmobile:read
/api/mobile/v1/reasoning/chains/{chain_id}GETallowmobile:read
/api/mobile/v1/events/streamGETallowmobile:read
/api/mobile/v1/alerts/policyGETallowmobile:read
/api/mobile/v1/sessionsPOSTallowmobile:session:create
/api/mobile/v1/sessions/{session_id}/endPOSTallowmobile:session:end
/api/mobile/v1/push/registerPOSTallow (feature-flagged)mobile:push
/api/mobile/v1/push/unregisterPOSTallow (feature-flagged)mobile:push
/api/mobile/v1/agents/{agent_id}/session/endPOSTdeny in v1N/A
/api/agent/* direct mutation routesPOSTdeny for mobile tokensN/A

Mode policy:

  • LAN and gateway use the same endpoint permissions.
  • Gateway requires TLS and strict cert validation.
  • Deny by default when required scope is missing.

Response Contract

For mobile endpoints, use a consistent envelope:

{
  "ok": true,
  "data": {},
  "meta": {
    "request_id": "req_...",
    "timestamp": "2026-02-19T19:00:00Z"
  }
}

Errors:

{
  "ok": false,
  "error": {
    "code": "unauthorized",
    "message": "invalid token"
  },
  "meta": {
    "request_id": "req_...",
    "timestamp": "2026-02-19T19:00:00Z"
  }
}

Endpoints (v1 Frozen)

All endpoints return the standard envelope. The data field for each is defined below.

Wave 1 Read-Only Parity Additions (2026-02-25)

The following additive endpoints are now part of /api/mobile/v1:

EndpointPurposeQuery ParamsResponse Shape
GET /tasksTask list + status countsstatus, agent_id, session_id, limit, search{tasks, counts}
GET /workflowsWorkflow summariesstatus, agent_id, limit{workflows, pending_approvals}
GET /workflows/{workflow_id}Workflow detail + timeline eventsnone{workflow, events}
GET /presencePresence + claim/worktree snapshotstatus, agent_id, limit{agents, claims, worktrees, summary}
GET /memory/statsMemory tier totalsnone{stats}
GET /memory/itemsMemory recall (read-only)tier, query, limit{items, tier}
GET /streamContext stream entriestypes, agent_id, session_id, limit{entries}
GET /topologyAgent topology graphnone{nodes, edges, clusters, updated_at}
GET /graph/statsGraph counts by typenone{stats}
GET /graph/entitiesEntity search/listtype, q, limit{entities}
GET /graph/pathPath between two entitiessource_id, target_id, max_depth{path}
GET /reasoning/chainsReasoning chain summariesstatus, limit{chains}
GET /reasoning/chains/{chain_id}Reasoning chain detailnone{chain}

Contract rules for these additions:

  • Additive-only fields (no breaking shape changes under v1).
  • Explicit array defaults ([] instead of null).
  • Status fields normalize unknown values to "unknown" where applicable.
  • All endpoints remain scope-gated by mobile:read and are denied outside /api/mobile/v1/*.

GET /api/mobile/v1/ping

Connectivity probe. Scope: mobile:read.

Response data:

{
  "pong": true
}

Source: internal/hud/api_mobile.go:171-176


GET /api/mobile/v1/dashboard

Mobile dashboard aggregate for quick app open. Scope: mobile:read.

Response data:

{
  "daemon_running": true,
  "server_count": 5,
  "active_sessions": 2,
  "active_agents": 3,
  "idle_agents": 1,
  "offline_agents": 0,
  "updated_at": "2026-02-23T12:00:00Z",
  "health": {
    "total_servers": 5,
    "healthy_servers": 4,
    "degraded_servers": 1,
    "down_servers": 0,
    "idle_servers": 0
  },
  "recent_timeline": [
    {
      "timestamp": "2026-02-23T11:59:00Z",
      "event_type": "agent.session.start",
      "agent_id": "claude-code",
      "agent_type": "claude-code",
      "data": {}
    }
  ]
}
FieldTypeDescription
daemon_runningboolWhether loomd is running
server_countintTotal registered MCP servers
active_sessionsintSessions with status active
active_agentsintAgents with presence status active
idle_agentsintAgents with presence status idle
offline_agentsintAgents with presence status offline
updated_atstring (RFC3339)Fleet snapshot timestamp
healthobjectServer health summary (see HealthSummary)
health.total_serversintTotal servers monitored
health.healthy_serversintServers passing health checks
health.degraded_serversintServers with intermittent failures
health.down_serversintServers failing health checks
health.idle_serversintServers with no recent activity
recent_timelinearrayLast 10 TimelineEntry objects

TimelineEntry schema:

FieldTypeDescription
timestampstring (RFC3339)Event time
event_typestringEvent type identifier
agent_idstringAgent that generated the event (omitted if N/A)
agent_typestringAgent type (omitted if N/A)
dataobjectEvent-specific payload

Source: internal/hud/api_mobile.go:178-212, internal/hud/monitor/fleet.go:18-63, internal/hud/monitor/health.go:98-105, internal/hud/eventlog.go:10-16


GET /api/mobile/v1/sessions

List all sessions. Scope: mobile:read.

Response data:

{
  "sessions": [
    {
      "id": "sess_abc123",
      "agent_id": "claude-code",
      "namespace": "loom-core/main",
      "status": "active",
      "description": "Working on mobile API",
      "started_at": "2026-02-23T10:00:00Z",
      "entry_count": 42,
      "total_tokens": 8500
    }
  ]
}

SessionInfo schema:

FieldTypeDescription
idstringSession identifier
agent_idstringOwning agent
namespacestringSession namespace
statusstringactive or ended
descriptionstringHuman-readable session description
started_atstring (RFC3339)Session start time
ended_atstring (RFC3339)Session end time (omitted if active)
entry_countintNumber of context entries
total_tokensintEstimated token usage

Source: internal/hud/api_mobile.go:214-225, internal/hud/bridge/agent.go:30-41


GET /api/mobile/v1/sessions/{session_id}

Single session detail. Scope: mobile:read.

Response data:

{
  "session": {
    "id": "sess_abc123",
    "agent_id": "claude-code",
    "namespace": "loom-core/main",
    "status": "active",
    "description": "Working on mobile API",
    "started_at": "2026-02-23T10:00:00Z",
    "entry_count": 42,
    "total_tokens": 8500
  }
}

Returns a single SessionInfo (same schema as above) under data.session.

Error cases:

  • 400 bad_request — missing session_id
  • 404 not_found — session not found

Source: internal/hud/api_mobile.go:227-252


GET /api/mobile/v1/sessions/{session_id}/events

Session-scoped event feed. Scope: mobile:read.

Query parameters:

ParamTypeDefaultMaxDescription
limitint100500Maximum events to return

Response data:

{
  "session_id": "sess_abc123",
  "events": [
    {
      "timestamp": "2026-02-23T11:00:00Z",
      "event_type": "agent.session.start",
      "agent_id": "claude-code",
      "agent_type": "claude-code",
      "data": {"session_id": "sess_abc123"}
    }
  ]
}
FieldTypeDescription
session_idstringEcho of requested session ID
eventsarrayTimelineEntry objects matching this session

Error cases:

  • 400 bad_request — missing session_id

Source: internal/hud/api_mobile.go:254-288


POST /api/mobile/v1/sessions

Create/start a new session. Scope: mobile:session:create.

Request body:

{
  "agent_id": "codex",
  "namespace": "loom-core/mobile",
  "description": "Investigate issue #123",
  "auto_recall": true
}
FieldTypeRequiredDescription
agent_idstringyesAgent to create the session for
namespacestringnoSession namespace
descriptionstringnoHuman-readable description
auto_recallboolnoAuto-recall previous context

Response: Delegates to internal handleAgentSessionStart handler. Returns the session envelope from the agent-context bridge.

Audit: Logs session_create with agent_id and namespace targets.

Source: internal/hud/api_mobile.go:297-321


POST /api/mobile/v1/sessions/{session_id}/end

End an active session. Scope: mobile:session:end.

Request body:

{
  "summarize": true
}
FieldTypeRequiredDescription
summarizeboolnoGenerate summary on end

Response: Delegates to internal handleAgentSessionEnd handler. Returns the session-end result from the agent-context bridge.

Error cases:

  • 400 bad_request — missing session_id or invalid body

Audit: Logs session_end with session_id and summarize targets.

Source: internal/hud/api_mobile.go:323-365


GET /api/mobile/v1/events/stream

SSE endpoint for mobile realtime feed. Scope: mobile:read.

Delegates to the existing /api/events SSE handler after auth validation.

Event allowlist in v1:

  • hud.fleet
  • hud.health
  • hud.workflows
  • hud.stream
  • agent.session.start
  • agent.session.end
  • agent.session.reaped
  • agent.heartbeat

Source: internal/hud/api_mobile.go:290-295


GET /api/mobile/v1/alerts/policy

Canonical event-to-severity-interruption-action matrix. Scope: mobile:read.

Mobile clients use this to synchronize notification behavior with the server-defined policy. The matrix defines how each SSE event type maps to severity, iOS interruption level, and allowed quick-actions.

Response data:

{
  "version": "v1",
  "policy": [
    {
      "event_type": "hud.health",
      "severity": "critical",
      "interruption_level": "time_sensitive",
      "title": "Server Down",
      "allowed_actions": ["view_dashboard", "acknowledge"],
      "conditional": true
    }
  ]
}

Policy entry schema:

FieldTypeDescription
event_typestringSSE event type this rule applies to
severitystringinfo, warning, or critical
interruption_levelstringpassive, active, time_sensitive, or critical
titlestringDisplay title for the alert
allowed_actionsarraySafe actions: view_session, view_dashboard, acknowledge
conditionalboolWhether severity depends on event payload (e.g., health events)

Interruption level semantics (maps to iOS UNNotificationInterruptionLevel):

LevelBehaviorUse case
passiveSilent; added to list without sound/bannerInfo-level events (session start/end, approvals)
activeDefault notification (sound + banner)Warnings requiring attention (reaped, degraded)
time_sensitiveBreaks through Focus/DNDCritical operational events (server down)
criticalReserved; not used in v1Emergency alerts (future)

Action constraints: All actions are read-only navigation operations. No mutation actions are permitted from alert quick-actions to maintain v1 scope discipline.

Source: internal/hud/api_mobile.go (handleMobileAlertsPolicy, mobileAlertPolicyMatrix)


POST /api/mobile/v1/push/register

Register a device push token for APNs or FCM notifications. Scope: mobile:push. Gated by --mobile-push-enabled feature flag (returns 404 when disabled).

Request body:

{
  "token": "device-push-token-hex-string",
  "platform": "apns"
}
FieldTypeRequiredDescription
tokenstringyesDevice push token from APNs or FCM
platformstringyes"apns" or "fcm"

Response data:

{
  "registered": true,
  "registration_id": "reg_a1b2c3d4"
}

Error cases:

  • 400 bad_request — missing/empty token, missing/invalid platform (must be apns or fcm)
  • 401 unauthorized — invalid bearer token
  • 403 forbidden — missing mobile:push scope
  • 404 not_found — push notifications not enabled (feature flag off)

Behavior: Re-registering the same token updates the last_used timestamp and device ID association. Tokens are stored in-memory for v1; persistent storage is planned for M5.

Source: internal/hud/api_mobile.go (handleMobilePushRegister)


POST /api/mobile/v1/push/unregister

Remove a device push token. Scope: mobile:push. Gated by --mobile-push-enabled feature flag.

Request body:

{
  "token": "device-push-token-hex-string"
}

Response data:

{
  "removed": true
}

The removed field is true if the token existed and was removed, false if the token was not found.

Error cases:

  • 400 bad_request — missing/empty token
  • 401 unauthorized — invalid bearer token
  • 403 forbidden — missing mobile:push scope
  • 404 not_found — push notifications not enabled (feature flag off)

Source: internal/hud/api_mobile.go (handleMobilePushUnregister)


Push Retry and Payload Policy (MBL-7)

The push notification infrastructure enforces the following contracts:

Retry classification by HTTP status:

StatusActionReason
2xxNo retrySuccess
400No retryBad request (fix payload)
401/403No retryAuth/provisioning error
404Invalidate tokenDevice not registered
410Invalidate tokenToken expired/unregistered
429Retry after delayRate limited (honor Retry-After or 30s default)
5xxRetry with backoffServer error

Backoff policy: Exponential (2^n * 1s base, capped at 5m, max 5 retries).

Payload guardrails: APNs and FCM payloads are validated against 4096-byte limits. Oversized payloads are truncated at the body field with UTF-8-safe "..." suffix. Truncation never breaks multi-byte characters.

Token lifecycle: Invalid tokens (404/410 from APNs) are automatically removed from the device token store. Stale tokens (not used within a configurable window) can be cleaned up via CleanupStale.

Source: internal/hud/mobile_push.go (ClassifyPushResponse, PushBackoffConfig, PushPayload.ValidateAndTruncate, DeviceTokenStore)


POST /api/mobile/v1/admin/revoke

Revoke a mobile operator token at runtime. Protected by admin token (X-Admin-Token header), not by mobile bearer auth.

Request headers:

  • X-Admin-Token: <admin_token> (required)

Request body:

{
  "token": "<mobile_token_to_revoke>"
}

Response data:

{
  "revoked": true
}

Error cases:

  • 400 bad_request — missing token field
  • 401 unauthorized — invalid admin token
  • 403 forbidden — admin token not configured

Audit: Logs token_revoke action.

Source: internal/hud/api_mobile.go (handleMobileAdminRevoke)


Rate Limiting

Mobile API endpoints enforce per-actor, per-minute rate limits:

CategoryDefault limitConfig flagEnv var
Mutation (POST)10 req/min--mobile-rate-limit-mutationHUD_MOBILE_RATE_LIMIT_MUTATION
Read (GET)60 req/min--mobile-rate-limit-readHUD_MOBILE_RATE_LIMIT_READ
  • Actor is identified by remote IP address.
  • Set limit to 0 to disable rate limiting for that category.
  • Rate-limited requests receive 429 Too Many Requests with error code rate_limited.

Device Identity

Mobile clients may include an X-Device-ID header on all requests. When present, the device ID is included in audit log entries for mutation operations. Maximum length: 128 characters (truncated if longer).


Internal Mapping

Mobile endpoints delegate to existing internal surfaces:

Mobile endpointInternal handler
POST /sessionshandleAgentSessionStart
POST /sessions/{id}/endhandleAgentSessionEnd
GET /sessionsAgentBridge.Sessions()
GET /sessions/{id}AgentBridge.Sessions() + filter
GET /sessions/{id}/eventsEventLog.All() + filter
GET /events/streamhandleSSE
GET /dashboardFleetMonitor.Snapshot() + HealthMonitor.Summary() + EventLog.All()
GET /pingDirect response
GET /alerts/policymobileAlertPolicyMatrix()
POST /push/registerDeviceTokenStore.Register()
POST /push/unregisterDeviceTokenStore.Invalidate()

The mobile API layer normalizes these into stable DTOs with the mobileEnvelope wrapper.

Idempotency and Retry

  • POST /sessions should be idempotent for same active session context.
  • POST /sessions/{id}/end should be safe to retry; “already ended/not found” should not cause destructive side effects.
  • Mobile client can retry transient network failures with bounded backoff.

Pagination and Limits

  • Default per_page: 30
  • Max per_page: 100
  • Cursor-based pagination can be added later if list volume grows.

Audit Requirements

All mutation endpoints must record:

  • actor id
  • device id
  • source mode (lan or gateway)
  • endpoint/action
  • target ids (session/agent)
  • outcome + error (if any)

Sources

  • docs/MOBILE_COMPANION_AUTH_BOOTSTRAP.md — consolidated auth bootstrap decision, flow descriptions, and LAN/gateway comparison
  • internal/hud/api_mobile.go — all mobile v1 handlers, envelope types, auth helpers
  • internal/hud/app.go:539-547 — route registration
  • internal/hud/bridge/agent.go:30-41SessionInfo struct
  • internal/hud/monitor/fleet.go:18-63FleetSnapshot struct
  • internal/hud/monitor/health.go:98-105HealthSummary struct
  • internal/hud/eventlog.go:10-16TimelineEntry struct
  • docs/STREAMABLE_HTTP.md
  • .loom/20-product-spec.md