Companion API (MVP)
April 2, 2026 · View on GitHub
Pairing
POST /v1/pairing/request
Creates a short-lived pairing session and returns pairingUri + qrDataUrl.
Response fields now also include scheme (http or https).
The QR payload includes the same scheme so the iPhone can confirm pairing over the same transport the companion is actually serving.
Release iPhone builds reject plaintext http pairings and require the companion to advertise https.
POST /v1/pairing/confirm
Body:
{
"pairingId": "...",
"nonce": "...",
"deviceName": "iPhone"
}
Requires local Mac confirmation prompt.
Response fields include the issued token, deviceId, and the confirmed scheme.
The iPhone stores that scheme with the paired host and derives ws or wss for /v1/stream from it.
If an older stored pairing only points at plaintext http, a release build clears it and asks the user to pair again with TLS enabled.
POST /v1/pairing/revoke
Requires bearer token. Revokes current device token or specific device by deviceId.
Data routes (require bearer token)
POST /v1/debug/ios-log
Stores the latest iPhone debug log in the local companion logs folder as logs/ios-device.ndjson.
GET /v1/projects
Returns grouped projects derived from Codex threads.
GET /v1/chats?projectId=...
Returns chats sorted by latest activity.
POST /v1/chats
Creates a new chat thread.
Body:
{
"cwd": "/Users/example/project"
}
The companion remembers the new project immediately so the mobile flow does not race thread/list.
POST /v1/chats/{chatId}/activate
Marks a chat as the active session before the iPhone starts streaming or sending follow-up turns.
Response:
{
"data": {
"chatId": "...",
"status": "already_active"
}
}
Supported status values:
already_activeresumedno_rollout
GET /v1/chats/{chatId}/messages
Returns persisted chat history from local Codex rollout files under ~/.codex/sessions.
Final assistant answers can include workedDurationSeconds, which the iPhone client uses to show a Worked for … divider above the final response.
GET /v1/chats/{chatId}/timeline
Returns the mobile chat timeline as:
- persisted rollout messages
- saved commentary history
- live and completed activity cards such as
Explored,Command finished,Edited file +X -Y,Context automatically compacted, andBackground terminal finished
Notes:
Context automatically compactedandBackground terminal finishedcome from visible Codex rollout history, so they survive chat reloads.- Mobile reconnect state such as
Reconnecting...is emitted by the iPhone client while the WebSocket stream retries and does not depend on persisted rollout history. - WebSocket auth uses the bearer token in the
Authorizationheader. The stream URL only carrieschatId. - The iPhone now uses
wssautomatically when the paired companion reportedhttpsduring pairing.
GET /v1/chats/{chatId}/run-state
Returns whether the selected chat currently has a running turn and, if available, the active turn id.
Response:
{
"data": {
"chatId": "...",
"isRunning": true,
"activeTurnId": "turn-123"
}
}
GET /v1/chats/{chatId}/pending-approval
Returns the latest still-open approval for the selected chat, or null when no approval is currently waiting.
This lets the iPhone re-hydrate an approval that first appeared on desktop before the mobile stream was connected.
POST /v1/dictation/transcribe
Transcribes recorded iPhone dictation through OpenAI audio transcriptions.
Body:
{
"audioBase64": "base64-encoded-audio",
"filename": "dictation.m4a",
"mimeType": "audio/m4a",
"language": "de"
}
Response:
{
"data": {
"text": "Transcribed text",
"model": "gpt-4o-transcribe"
}
}
Notes:
- The companion reads
OPENAI_API_KEYfrom.env,.env.local, or the process environment. OPENAI_TRANSCRIPTION_MODELdefaults togpt-4o-transcribe.OPENAI_BASE_URLdefaults tohttps://api.openai.com/v1.
POST /v1/chats/{chatId}/messages
Starts a turn with text input.
Body:
{
"text": "Please inspect the latest diff."
}
Response:
{
"data": {
"chatId": "...",
"turnId": "turn-123"
}
}
POST /v1/chats/{chatId}/steer
Sends immediate follow-up input into the active turn.
Body:
{
"text": "Search in apps/ios first."
}
Notes:
- If the chat still has an active turn, the companion calls Codex
turn/steer. - If the run has already ended in the meantime, the companion falls back to a normal
turn/startso the user does not lose the follow-up.
POST /v1/chats/{chatId}/stop
Interrupts the current active turn.
Response:
{
"data": {
"chatId": "...",
"interrupted": true,
"turnId": "turn-123"
}
}
GET /v1/projects/{projectId}/context
Returns local Codex runtime context plus Git summary for the selected project.
Response shape:
{
"data": {
"projectId": "...",
"cwd": "/Users/example/project",
"runtimeMode": "local",
"approvalPolicy": "on-request",
"sandboxMode": "workspace-write",
"model": "gpt-5-codex",
"modelReasoningEffort": "high",
"trustLevel": "trusted",
"git": {
"isRepository": true,
"branch": "main",
"changedFiles": 2,
"stagedFiles": 1,
"unstagedFiles": 1,
"untrackedFiles": 0,
"changedPaths": []
}
}
}
GET /v1/projects/{projectId}/git/branches
Returns all local branches in the selected repository.
GET /v1/projects/{projectId}/git/diff
Returns a combined Git diff.
Optional query:
path=README.mdfor a single file diff
Notes:
- The response includes staged and unstaged patch text.
- Untracked files are listed separately.
- Large patch output is truncated and marked with
truncated: true.
POST /v1/projects/{projectId}/git/checkout
Checks out an existing local branch.
Body:
{
"branch": "feature/mobile-shell"
}
POST /v1/projects/{projectId}/git/commit
Creates a Git commit from already staged changes.
Body:
{
"message": "Refine remote mobile shell"
}
If nothing is staged, the route returns a 400 error.
PATCH /v1/runtime/config
Updates top-level runtime settings in ~/.codex/config.toml.
Body:
{
"approvalPolicy": "on-request",
"sandboxMode": "workspace-write"
}
Supported approvalPolicy values:
untrustedon-failureon-requestnever
Supported sandboxMode values:
read-onlyworkspace-writedanger-full-access
POST /v1/approvals/{approvalId}
Body:
{ "decision": "approve" }
Supported decisions: approve, decline, allow_for_session, allow_always.
Streaming
GET /v1/stream?chatId=...
WebSocket endpoint with events:
turn_startedmessage_deltaitem_starteditem_completedapproval_requiredapproval_clearedturn_completederror
approval_required payload now includes:
kind:command,fileChange, ormcpmode:approvalormcp_elicitationtitlesummaryriskLevelcreatedAt- optional
serverName supportsSessionAllowsupportsAlwaysAllow
approval_cleared payload includes:
ids: array of approval ids that are no longer pending for that chat