| title | JSON-RPC |
|---|---|
| description | Long-running JSON-RPC 2.0 over stdio for chats, history, watch, and send — same surfaces as the CLI, one process. |
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.
- 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.
- 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.
Params:
limit(int, default 20)unread_only(bool, defaultfalse) — when true, return only chats withunread_count > 0; unavailable database schemas return an invalid-params error rather than an empty list
Result:
{ "chats": [Chat] }Params:
chat_id(int, optional)time_zone(IANA identifier, optional; defaults to the local timezone)include_media(bool, defaultfalse)
Result:
{
"total_messages": 123,
"sent_messages": 60,
"received_messages": 63,
"time_zone": "Europe/Vienna",
"chats": [],
"senders": [],
"services": [],
"dates": []
}When media is requested, media includes distinct attachment totals and bytes grouped by
UTI/MIME and chat. Otherwise the media key is omitted. Invalid, non-positive, or nonexistent
chat_id values return invalid params rather than widening to all chats.
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] }Reads future outbound Send Later rows from chat.db. This method is read-only and does not require the IMCore bridge.
Params:
limit(positive int, default 50)
Result:
{ "messages": [ScheduledMessage] }Older Messages database schemas without scheduling columns return an invalid-params error rather than an ambiguous empty list.
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.
If a live all-chat row appears before Messages has joined it to a chat, RPC watch retries it briefly and then drops it fail-closed instead of emitting an empty chat_id=0 direct-message-shaped payload.
Params:
subscription(int, required)
Result:
{ "ok": true }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}.
Params:
guid(string, required) — outgoing message GUID.
Result:
{
"ok": true,
"guid": "8DF...",
"send_state": "delivered",
"service": "iMessage",
"checked_at": "2026-05-28T20:43:00Z",
"delivered_at": "2026-05-28T20:42:58Z",
"status_fields": {
"is_sent": true,
"is_delivered": true,
"is_finished": true,
"error": 0,
"date_delivered": "2026-05-28T20:42:58Z",
"date_read": null,
"is_delayed": false,
"is_prepared": false,
"is_pending_satellite_send": false,
"was_downgraded": false
}
}send_state is normalized to pending, sent, delivered, or failed.
Missing rows return pending with status_fields: null.
These methods require the IMCore bridge and target an existing chat with chat_id, chat_identifier, or chat_guid.
send.richsends text with optionaleffect,subject,reply_to,part_index,dd_scan, andtext_formatting. Alternatively, pass only one chat target plus an HTTP(S)urlto send an Apple URL-preview balloon. URL mode is iMessage-only and rejects text/send modifiers; metadata or image lookup failure falls back to a metadata-only card, never a plain-message send.send.attachmentsendsfileorpath, with optionalaudio/is_audio/as_voice.tapbacksends or removes a reaction. Params:message_idormessage_guid, plusreaction/kind/emoji, optionalremove.message.editeditsmessage_id/message_guidwithtext.message.unsend,message.delete, andmessage.notifyAnywaystargetmessage_id/message_guid.contacts.shouldShareContactreads Apple Messages' advisory Name & Photo offer eligibility. The result includescan_inspect_offer,can_share, and tri-stateshould_offer.contacts.shareContactCardexplicitly requests Apple Messages Name & Photo sharing. Despite the compatibility name, this does not send a vCard. Success reportsrequested: true, not delivery.
The two contacts.* compatibility methods accept chat_id, chat_identifier,
or chat_guid. Sharing discloses the local Messages profile to every chat
participant and must only be invoked after explicit user confirmation.
Result:
{ "ok": true }send.rich and send.attachment return guid / message_id when the bridge reports the sent message GUID.
Requires the IMCore bridge.
Params:
address(string, required) — phone number or email address.alias_type(phone|email, optional) — inferred fromaddresswhen omitted.service(iMessage, optional) — SMS checks are rejected.
Result:
{
"ok": true,
"address": "+14155551212",
"alias_type": "phone",
"destination": "tel:+14155551212",
"id_status": 1,
"available": true,
"service": "iMessage"
}poll.send creates a native Apple Messages Polls extension balloon through the IMCore bridge. The bridge must be injected with imsg launch; the AppleScript transport cannot send native extension payloads. Messages does not render the poll payload title on the balloon, so poll.send also sends a best-effort plain caption message right after the poll. The caption defaults to question; pass comment when the visible caption should differ from the stored poll question.
Request:
{"jsonrpc":"2.0","id":"poll","method":"poll.send","params":{"chat_id":42,"question":"Dinner?","options":["Pizza","Sushi"]}}With a caption override:
{"jsonrpc":"2.0","id":"poll","method":"poll.send","params":{"chat_id":42,"question":"Dinner?","comment":"Vote by 5pm","options":["Pizza","Sushi"]}}Response:
{"ok":true,"event":"imessage.poll.created","guid":"...","message_id":"...","poll":{"kind":"created","event":"imessage.poll.created","question":"Dinner?","options":[{"id":"...","text":"Pizza"},{"id":"...","text":"Sushi"}]}}poll.vote casts a native vote after validating the poll and option against local history.
polls.unvote removes a selection with the same poll/option parameters:
{"jsonrpc":"2.0","id":"vote","method":"poll.vote","params":{"chat_id":42,"poll_guid":"POLL-GUID","option_id":"OPTION-UUID"}}
{"jsonrpc":"2.0","id":"unvote","method":"polls.unvote","params":{"chat_id":42,"poll_guid":"POLL-GUID","option_id":"OPTION-UUID"}}messages.poll.send is accepted as an alias for poll.send. The caption echo is deliberately best-effort: if the poll is created but the follow-up caption send fails, the RPC still returns the poll result to avoid retrying and creating a duplicate poll.
send.sticker sends a validated image file as a sticker-attributed IMCore
transfer. The bridge must be injected with imsg launch; AppleScript cannot
preserve sticker attribution. Stickers are iMessage-only. Accepted images are
PNG/APNG, GIF, or JPEG, at most 500 KiB, 618x618 pixels, 100 frames, and
25 million total decoded pixels.
Request:
{"jsonrpc":"2.0","id":"sticker","method":"send.sticker","params":{"chat_id":42,"file":"~/Desktop/sticker.png","attach_to":"MESSAGE_GUID","part_index":0}}Response:
{"ok":true,"transfer_guid":"..."}guid and message_id are included when Messages exposes the newly queued
message immediately; treat them as best-effort. transfer_guid is returned on
every successful bridge send.
Use exactly one of chat_id, chat_identifier, or chat_guid. attach_to
accepts a bare message GUID or p:N/GUID; part_index must agree with an
embedded part and is invalid without attach_to. Unknown parameters and
non-object params fail with invalid params rather than falling back.
See JSON output → Chat list item. Every field documented there appears in the RPC chats.list response.
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).
Native Apple Messages polls are emitted by messages.history and watch.subscribe with the same poll object documented in JSON output → Native poll extension. For inbound native polls whose payload title is empty, imsg backfills poll.question from the earliest clean caption row that replies to the poll.
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.
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.