Docs
REST API reference
Send and read iMessages programmatically. Connect any app, CRM, or agent.
Base URL
All endpoints below are relative to this host. HTTPS only.
Authentication
Every /v1/* request must carry your key in the X-API-KEY header (Authorization: Bearer <key> also works).
X-API-KEY: <your-api-key>Keep it secret — anyone holding this key can read and send your iMessages. Manage or regenerate it in Settings inside the dashboard.
Errors, limits & conventions
- Every success response is wrapped in a
dataenvelope; every failure in anerrorenvelope. - Identifiers: conversations, drafts, and contacts are addressed by their server-generated
id(collision-proof). Messages are addressed bymessageGUID— the stable iMessage UUID — always scoped to your account. - Case & naming: input is snake_case (
phone_number,display_name,photo_url); responses are camelCase. API-created contacts are markedsource=manualso the Mac connector never overwrites your edits. - Phone numbers are stored E.164 (
+14155551234). The stored field spelledrefrenceis returned asreferenceby the API.
Error envelope example
{ "error": { "code": "unauthorized", "message": "Invalid API key." } }Quickstart — send your first iMessage
curl -X POST \
https://automessage-api-tfsfyvwypq-uc.a.run.app/v1/drafts \
-H "X-API-KEY: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"to":"+14155551234","body":"Hello from the autoMessage API"}'Sending is asynchronous: you create a draft, your Mac connector delivers it over iMessage, then the draft records the outcome.
Response
{
"data": {
"id": "aB3dE5…",
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"sent": false,
"sendError": false,
"sentMessage": null
}
}Prefer AI tools over raw HTTP?
The autoMessage MCP server gives Claude, Cursor, Codex, and any MCP client these same capabilities as native tools — install it in 30 seconds.
Conversations
Read-only. Threads are created by real iMessage activity.
GET/v1/conversationsList or search your conversations, newest activity first.
QueryPass at most one primary filter — participant (phone/email in the thread; matches 1:1 AND groups; loose formats like "(502) 544-2686" are normalized), name (case-insensitive display-name substring), handle (exact 1:1 recipient), group (true/false), or unread (true). All also accept activeSince / activeBefore (ISO-8601), limit (default 50, max 200), and cursor (from pagination.nextCursor). Combining primaries returns 400.
Response
{
"data": [ {
"id": "zjueBDrNP2490J6JPWnJ",
"displayName": "+14155551234",
"groupName": "",
"participants": ["+14155551234"],
"previewMessage": "See you at 2pm!",
"unreadCount": 1,
"lastActive": "2026-07-16T17:13:29.497Z",
"contacts": [],
"owner": "users/<your-uid>"
} ]
}GET/v1/conversations/{id}Fetch a single conversation by its id.
Response
{ "data": {
"id": "zjueBDrNP2490J6JPWnJ",
"displayName": "+14155551234",
"groupName": "",
"participants": ["+14155551234"],
"previewMessage": "See you at 2pm!",
"unreadCount": 1,
"lastActive": "2026-07-16T17:13:29.497Z",
"contacts": [],
"owner": "users/<your-uid>"
} }GET/v1/conversations/{id}/messagesPaginated messages in a thread, with sender names resolved from your contacts.
Querylimit (default 50) · cursor = the sentDate of the last item from the previous page (ISO 8601). Response includes nextCursor when more pages exist.
Response
{
"data": [ {
"id": "0NQXTM0s4ZgJGqrv4qbK",
"messageGUID": "8328F671-7D1D-B351-EF5E-B0963FD80FF1",
"conversationGUID": "zjueBDrNP2490J6JPWnJ",
"sender": "+14155551234",
"senderName": "Jane Appleseed",
"body": "See you at 2pm!",
"type": "iMessage",
"outbound": false,
"read": true,
"sentDate": "2026-07-16T17:13:29.497Z",
"dateRead": null,
"dateDelivered": null,
"cacheHasAttachments": false,
"attachments": [],
"tapbacks": [],
"replyToGUID": "",
"unsent": false,
"reference": ""
} ],
"nextCursor": "2026-07-16T17:13:29.497Z"
}Messages
Read-only. Individual messages are addressed by their iMessage GUID.
GET/v1/messages/{messageGUID}Look up any single message you own by its iMessage GUID.
Response
{ "data": {
"id": "0NQXTM0s4ZgJGqrv4qbK",
"messageGUID": "8328F671-7D1D-B351-EF5E-B0963FD80FF1",
"conversationGUID": "zjueBDrNP2490J6JPWnJ",
"sender": "+14155551234",
"senderName": "Jane Appleseed",
"body": "See you at 2pm!",
"type": "iMessage",
"outbound": false,
"read": true,
"sentDate": "2026-07-16T17:13:29.497Z",
"dateRead": null,
"dateDelivered": null,
"cacheHasAttachments": false,
"attachments": [],
"tapbacks": [],
"replyToGUID": "",
"unsent": false,
"reference": ""
} }Drafts — sending iMessages
Create, list, poll, edit, and cancel outbound drafts.
POST/v1/draftsQueue an iMessage for delivery.
Request body
{
"to": "+14155551234",
"body": "Hello from the autoMessage API"
}Response
{ "data": {
"id": "aB3dE5fGh7…",
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"sent": false,
"sendError": null,
"draftDate": "2026-07-16T18:00:00.000Z",
"sentDate": null,
"sentMessage": null,
"responseWebhook": null,
"owner": "users/<your-uid>"
} }GET/v1/draftsList your drafts, newest first.
Querylimit (default 200).
Response
{
"data": [ {
"id": "aB3dE5fGh7…",
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"sent": true,
"sendError": null,
"draftDate": "2026-07-16T18:00:00.000Z",
"sentDate": "2026-07-16T18:00:07.412Z",
"sentMessage": "conversations/zjueBD…/messages/0NQXTM…",
"responseWebhook": null,
"owner": "users/<your-uid>"
} ]
}GET/v1/drafts/{id}Poll delivery status.
Response
{ "data": {
"id": "aB3dE5fGh7…",
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"sent": true,
"sendError": null,
"draftDate": "2026-07-16T18:00:00.000Z",
"sentDate": "2026-07-16T18:00:07.412Z",
"sentMessage": "conversations/zjueBD…/messages/0NQXTM…",
"responseWebhook": null,
"owner": "users/<your-uid>"
} }PATCH/v1/drafts/{id}Edit a draft that has not sent yet.
Request body
{
"body": "Updated message text"
}Response
{ "data": {
"id": "aB3dE5fGh7…",
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"sent": false,
"sendError": null,
"draftDate": "2026-07-16T18:00:00.000Z",
"sentDate": null,
"sentMessage": null,
"responseWebhook": null,
"owner": "users/<your-uid>"
} }DELETE/v1/drafts/{id}Cancel an unsent draft (soft-cancel — the record is kept, sendError becomes "cancelled").
Response
{
"data": {
"id": "aB3dE5fGh7…",
"sent": false,
"sendError": "cancelled",
"cancelledAt": "2026-07-16T18:02:11.000Z"
}
}Contacts
Manage your address book from code. API-created rows are marked source=manual.
GET/v1/contactsList your contacts.
Querylimit (default 200).
Response
{
"data": [ {
"id": "<your-uid>__+14155551234",
"displayName": "Jane Appleseed",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"photoURL": "",
"userName": "",
"url1": "",
"source": "manual",
"wouldSyncBack": false,
"createdTime": "2026-07-16T18:00:00.000Z",
"editedTime": null,
"owner": "users/<your-uid>"
} ]
}POST/v1/contactsCreate or update a contact (idempotent upsert per handle).
Request body
{
"phone_number": "+14155551234",
"display_name": "Jane Appleseed",
"email": "jane@example.com",
"photo_url": "https://example.com/jane.jpg"
}Response
{ "data": {
"id": "<your-uid>__+14155551234",
"displayName": "Jane Appleseed",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"photoURL": "",
"userName": "",
"url1": "",
"source": "manual",
"wouldSyncBack": false,
"createdTime": "2026-07-16T18:00:00.000Z",
"editedTime": null,
"owner": "users/<your-uid>"
} }GET/v1/contacts/{id}Fetch a single contact.
Response
{ "data": {
"id": "<your-uid>__+14155551234",
"displayName": "Jane Appleseed",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"photoURL": "",
"userName": "",
"url1": "",
"source": "manual",
"wouldSyncBack": false,
"createdTime": "2026-07-16T18:00:00.000Z",
"editedTime": null,
"owner": "users/<your-uid>"
} }PATCH/v1/contacts/{id}Update fields on a contact (also forces source=manual).
Request body
{
"display_name": "Jane A."
}Response
{ "data": {
"id": "<your-uid>__+14155551234",
"displayName": "Jane Appleseed",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"photoURL": "",
"userName": "",
"url1": "",
"source": "manual",
"wouldSyncBack": false,
"createdTime": "2026-07-16T18:00:00.000Z",
"editedTime": null,
"owner": "users/<your-uid>"
} }DELETE/v1/contacts/{id}Delete a contact.
Response
{ "data": { "id": "<your-uid>__+14155551234", "deleted": true } }Response webhooks
Attach a responseWebhook to a draft and, after that draft sends, the subscription is registered on the matched conversation. Any future inbound message in that conversation posts a JSON payload to your URL. The URL must be a public HTTPS endpoint — private and internal addresses are rejected.
Verifying signatures
When you register a webhook, the response returns a meta.webhookSecret (prefixed whsec_) shown only once — store it. Every delivery carries an X-AutoMessage-Signature header of the form t=<timestamp>,v1=<hmac>. Recompute it as HMAC-SHA256(secret, "<timestamp>.<raw-body>") and compare in constant time; reject old timestamps to prevent replays.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const [t, v1] = header.split(',').map(p => p.split('=')[1]);
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
const fresh = Math.abs(Date.now()/1000 - Number(t)) < 300; // 5 min
return ok && fresh;
}POST/v1/draftsDraft body with an attached responseWebhook.
Request body
{
"to": "+14155551234",
"body": "Hello from the autoMessage API",
"responseWebhook": {
"url": "https://your.app/hooks/imessage"
}
}Status
API health, your account info, and whether your Mac connector is online (the agent must be running to deliver messages).
GET/v1/statusHealth snapshot for API, account, and connector.
Response
{
"data": {
"api": { "service": "automessage-api", "version": "0.1.0", "healthy": true },
"user": { "uid": "<your-uid>", "email": "you@example.com", "phone_number": "14155551234" },
"connector": { "online": true }
}
}Schemas & machine-readable spec
Full OpenAPI 3.1 spec: /openapi.yaml. MCP install guide: /docs/mcp/.
Note the legacy POST /v1/messages endpoint is a deprecated alias for POST /v1/drafts. Still accepted; behaves identically. New integrations should use /v1/drafts.