Parlane

REST contract

MCP is the primary transport. This is the fallback for everything else: a vibe-coder can implement it in about 50 lines straight from the schemas, and the live demo agent is a working reference to test against. Auth is a bearer token on every request.

GET /.well-known/parlane.json

Returns the manifest. 200 + application/json. See the manifest reference.

POST /chat → SSE stream

POST /chat
{
  "conversation_id": "conv_123",
  "message": "What's the reactor load?",
  "attachments": []
}

conversation_id is client-managed; your server may override it by returning a different id in the done event. Response: Content-Type: text/event-stream, one SSE event per line-group. The event types are the contract:

eventdatameaning
delta{ text }An incremental chunk of assistant text. Concatenate in order.
tool{ tool, status, label? }Tool-call activity indicator.
done{ conversation_id, message_id? }End of turn.
error{ code, message }Terminal error; the stream ends.
example stream
event: delta
data: {"text": "Reactor load is holding at "}

event: delta
data: {"text": "62%."}

event: tool
data: {"tool": "check_reactor", "status": "start", "label": "Checking reactor…"}

event: tool
data: {"tool": "check_reactor", "status": "end"}

event: done
data: {"conversation_id": "conv_123", "message_id": "msg_9"}

The stream must end with exactly one done or one error. Clients paint the first delta within 150 ms of receipt. That's the target for a responsive-feeling composer, not a hard guarantee your network will hit.

GET /ui/{dashboard_id}

Returns a UI document (see the component catalog). 200 + application/json. dashboard_id matches a manifest dashboards[].id.

POST /action → { ui? | ack? }

POST /action
{
  "action_id": "a1b2c3d4-0001",
  "tool": "set_environment",
  "dashboard_id": "systems",
  "params": {
    "source": "systems-dashboard",
    "scrubber": true,
    "targetTemp": 21.5
  }
}

Response: exactly one of ui / ack is meaningful:

{
  "ui": {
    "version": 1,
    "id": "systems",
    "root": { "type": "stack", "props": { "direction": "vertical" }, "children": [] }
  }
}
// or, when nothing needs to change on screen:
{ "ack": true }

If ui is present, the client replaces the current dashboard in place (diffed by id): it redraws the same tab and does not navigate to another tab or screen. Otherwise it applies the action envelope's refresh. A failed tool returns { error: { code?, message, details? } }, surfaced non-fatally. There is no patch response: full-replace only.

If a control does nothing, an action returns 200 but the UI doesn't change, or a tool runs twice, work through the action troubleshooting table in the integration kit §4.6. Two common ones: to update what's on screen, return the full { ui } or set refresh, not { ack: true }; and make your tool idempotent on action_id, since the client can retry.

For a rejected share, use the standard actionable codes (with a 4xx status) so the failure tells the user what to fix instead of a bare "Server error":unsupported_kind { kind } (415), unsupported_mime { mime, accepted? } (415), or attachment_too_large { maxBytes } (413). Codes are recommended, not required. An unknown or absent code falls back to message. See the integration kit §8.

HTTP/1.1 415 Unsupported Media Type
{
  "error": {
    "code": "unsupported_mime",
    "message": "Word documents aren't supported here. Accepted: image/png, image/jpeg, application/pdf.",
    "details": {
      "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "accepted": ["image/png", "image/jpeg", "image/webp", "application/pdf"]
    }
  }
}

Acknowledge a share fast, then process it. The user is waiting on the share request. Validate the attachment, persist it durably, and return 200 as soon as it's stored; do OCR, indexing, sync, or agent work asynchronously after you respond. Don't couple the response to that slow work: a 502 means an upstream timed out or crashed mid-processing, and a 413 means the payload exceeded a body limit (raise the limit past 20 MB for base64 overhead).

POST /notify

Relay-side only. Your app-facing server never implements this endpoint; itcalls it, on the Parlane-operated hosted relay, to send a push. You never supply Apple credentials, and a self-hosted relay can't deliver push to the Parlane app. Test push against a TestFlight build (production APNs), not a local dev build (sandbox token). See relay self-hosting and the integration kit §3a.1.

Auth

Bearer token on every request: Authorization: Bearer <token>. A server may print a QR encoding both its URL and a revocable, regenerable token so connecting is a single scan:

parlane://connect?url=https%3A%2F%2Fyour-server.example.com&token=sk_live_...

Attachments

v1 = images, text, URLs, and documents (PDF, Office, and similar), with a 10 MB cap per attachment. Validate against spec/attachment.schema.json.

fieldtypemeaning
kind"image"|"text"|"url"|"file"Attachment type. file = documents (PDF, Word, Excel, …) for the AI to process.
mimestringMIME type (required for all kinds), for example image/png, application/pdf.
namestring?Original filename, when known.
datastring?Base64 payload for image/text/file. ≤ 10 MB decoded.
urlstring?For kind: "url", or a pre-uploaded blob URL instead of inline data.

Exactly one of data / url is present.

Next

Relay self-hosting →