imsg rpc exposes the read and send surfaces over JSON-RPC 2.0 on stdin/stdout. It's designed for agents and gateways that want a single long-lived process for chats, history, send, and watch — without a TCP port, daemon, or system service.
#Transport
- One JSON object per line on stdin (request) and stdout (response/notification).
- JSON-RPC 2.0 framing:
jsonrpc,id,method,params. - Notifications omit
id. - Stderr is reserved for human-readable diagnostics.
- Startup failures such as missing Full Disk Access are returned as JSON-RPC
errors on the first request instead of human-readable stdout banners.
#Lifecycle
- The host process spawns one
imsg rpcchild. - The child stays alive across many requests and one-or-more watch subscriptions.
- No TCP port. No launch agent. No
imsgdaemon to install.
The pattern intentionally mirrors language servers and the way imsg's parent gateway (Clawdis) supervises subprocesses — a single signal-style child that exits cleanly when stdin closes.
#Methods
#chats.list
Params:
limit(int, default 20)
Result:
{ "chats": [Chat] }
#messages.history
Params:
chat_id(int, required) — preferred identifier.limit(int, default 50)participants(array of handle strings, optional)start/end(ISO 8601, optional)attachments(bool, defaultfalse)
Result:
{ "messages": [Message] }
#watch.subscribe
Params:
chat_id(int, optional) — omit for all-chat stream.since_rowid(int, optional) — exclusive cursor.participants(array, optional)start/end(ISO 8601, optional)attachments(bool, defaultfalse)include_reactions(bool, defaultfalse)debounce_ms(int, default500)
Result:
{ "subscription": 1 }
Notifications (one per emitted message):
{
"jsonrpc": "2.0",
"method": "message",
"params": {
"subscription": 1,
"message": { ... }
}
}
The RPC default debounce (500ms) is intentionally higher than the CLI default (250ms). RPC's typical caller is an agent that just sent a message and is waiting for the inbound echo to settle (is_from_me correction, attachment metadata, …). 500ms is enough for those follow-ups to land before the message is emitted.
Like the CLI watch, RPC watch backs filesystem events with a low-frequency poll so a missed event or a rotated SQLite sidecar doesn't leave the subscription silent.
#watch.unsubscribe
Params:
subscription(int, required)
Result:
{ "ok": true }
#send
Params (direct send):
to(string, required)text(string, optional)file(string, optional)service(imessage|sms|auto, optional)region(string, optional)
Params (chat target):
chat_idorchat_identifierorchat_guid— exactly one.chat_idis preferred.text/fileas above.
Result:
{ "ok": true, "id": 1979, "guid": "8DF..." }
id and guid are best-effort. send returns them when the inserted row can be observed in chat.db after Messages accepts the send. Attachment-only sends, delayed database writes, or ambiguous direct sends may return only {"ok": true}.
For chat-target sends, send also performs the Tahoe ghost-row check: if Messages writes an empty unjoined SMS row instead of delivering, the call returns an error rather than {"ok": true}.
#Objects
#Chat
See JSON output → Chat. Every field documented there appears in the RPC chats.list response.
#Message
See JSON output → Message. When include_reactions: true, message notifications also include the reaction extension fields (is_reaction, reaction_type, reaction_emoji, is_reaction_add, reacted_to_guid).
account_id, account_login, last_addressed_handle, and outgoing destination_caller_id are read-only routing diagnostics; the AppleScript send API does not expose a from selector.
#Examples
Request chats.list:
{"jsonrpc":"2.0","id":"1","method":"chats.list","params":{"limit":10}}
Response:
{"jsonrpc":"2.0","id":"1","result":{"chats":[...]}}
Subscribe to a chat:
{"jsonrpc":"2.0","id":"2","method":"watch.subscribe","params":{"chat_id":1}}
Notification on each new message:
{"jsonrpc":"2.0","method":"message","params":{"subscription":2,"message":{...}}}
Send and receive verification:
{"jsonrpc":"2.0","id":"3","method":"send","params":{"to":"+14155551212","text":"hi"}}
{"jsonrpc":"2.0","id":"3","result":{"ok":true,"transport":"applescript","id":1979,"guid":"8DF..."}}
send accepts transport: "auto" | "bridge" | "applescript". auto uses the IMCore bridge for existing chats when it is running, then falls back to AppleScript. Use bridge when the caller requires private-API delivery and should fail instead of falling back.