Integrate

JSON-RPC

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 rpc child.
  • The child stays alive across many requests and one-or-more watch subscriptions.
  • No TCP port. No launch agent. No imsg daemon 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, default false)

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, default false)
  • include_reactions (bool, default false)
  • debounce_ms (int, default 500)

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_id or chat_identifier or chat_guid — exactly one. chat_id is preferred.
  • text / file as 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.