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:
| event | data | meaning |
|---|---|---|
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. |
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.
| field | type | meaning |
|---|---|---|
kind | "image"|"text"|"url"|"file" | Attachment type. file = documents (PDF, Word, Excel, …) for the AI to process. |
mime | string | MIME type (required for all kinds), for example image/png, application/pdf. |
name | string? | Original filename, when known. |
data | string? | Base64 payload for image/text/file. ≤ 10 MB decoded. |
url | string? | For kind: "url", or a pre-uploaded blob URL instead of inline data. |
Exactly one of data / url is present.