# messages.dev > messages.dev is the iMessage API for agents. Send and receive iMessages, attachments, audio messages, reactions, typing indicators, and read receipts. The recommended integration is the TypeScript SDK (`@messages-dev/sdk`); a plain REST API is available for any other language. Outbound messages follow a contact-first model: the recipient (or group chat) must message your line before you can send to them. The sandbox line is exempt once activated. This file inlines auth, every endpoint, and every webhook event payload so an integrator can skip the multi-fetch dance. For deeper guides see [/llms-full.txt](https://messages.dev/llms-full.txt) (auto-generated, full content) or the human-readable docs at https://messages.dev/docs. ## Recommended: TypeScript SDK ```bash npm install @messages-dev/sdk ``` ```typescript import { createClient } from "@messages-dev/sdk"; const client = createClient(); // reads MESSAGES_API_KEY await client.sendMessage({ from: "+15551234567", to: "+15559876543", text: "Hello!", }); ``` Every endpoint below has a typed wrapper. Pagination returns `{ data, hasMore, nextCursor }` and supports `for await…of`. Errors throw typed exceptions (`AuthenticationError`, `AuthorizationError`, `InvalidRequestError`, `NotFoundError`, `RateLimitError`). ## Auth ``` Authorization: Bearer sk_live_... ``` Keys are created in the dashboard. Each key carries a set of scopes that gate which endpoints it can call: - `messages:read` — `GET /v1/messages` - `messages:write` — `POST /v1/messages`, `POST /v1/audio-messages` - `chats:read` — `GET /v1/chats` - `lines:read` — `GET /v1/lines` - `reactions:read` — `GET /v1/reactions` - `reactions:write` — `POST /v1/reactions` - `typing:read` / `typing:write` — `GET /v1/typing`, `POST /v1/typing` - `receipts:read` / `receipts:write` — `GET /v1/receipts`, `POST /v1/receipts` - `webhooks:read` / `webhooks:write` — `GET /v1/webhooks`, `POST /v1/webhooks`, `DELETE /v1/webhooks` - `outbox:read` — `GET /v1/outbox` - `files:read` / `files:write` — `GET /v1/files`, `POST /v1/files` A key can also be restricted to specific lines (only requests with `from` matching one of those lines are authorized). ## Conventions - Base URL: `https://api.messages.dev` - All endpoints are prefixed with `/v1` and return JSON. - Wire format is `snake_case`. The TypeScript SDK is `camelCase`. - IDs include a type prefix: `msg_`, `cht_`, `ln_`, `rxn_`, `obx_`, `wh_`, `ind_`, `rcp_`, `file_`, `dlv_`. - Timestamps are Unix milliseconds (UTC). - Every response includes `request_id`; include it when reporting issues. ### Async writes `POST /messages`, `POST /audio-messages`, `POST /reactions`, `POST /typing`, and `POST /receipts` are asynchronous. They return `{ id: "obx_...", status: "pending", request_id }`. Track delivery via webhooks (`message.sent`) or by polling `GET /v1/outbox?id=obx_...`. ### Pagination List endpoints accept `limit` (default 50, max 100) and `cursor`. Responses include `has_more` and `next_cursor`. Pass `next_cursor` as the next request's `cursor` until `has_more` is false. ### Contact-first restriction Outside the sandbox, the recipient (or group chat) must message your line first. Sending to a cold contact returns `403 contact_has_not_messaged`. ### Rate limits Per-API-key hourly limit (default 1000/hour). When exceeded the API returns `429` with a `Retry-After` header. Per-line iMessage volume guidance lives in https://messages.dev/docs/scaling/limits. ## Endpoints ### `GET /v1/lines` List your lines. No params. Response: `{ data: Line[], has_more, next_cursor, request_id }` where `Line = { id: ln_..., handle, label?, service: "imessage"|"sms"|"auto", is_active }`. ### `GET /v1/chats?from=&limit=&cursor=` List chats on a line, ordered by most recent activity. `Chat = { id: cht_..., line_id, identifier, service, name?, is_group?, participants?, last_message_at? }`. ### `GET /v1/messages?from=&to=&limit=&cursor=` List messages in a chat. `to` accepts a phone number, Apple ID, or `cht_...` chat ID. `Message = { id: msg_..., line_id, chat_id, guid, sender, text|null, attachments: Attachment[], service|null, is_from_me, is_audio_message|null, sent_at, synced_at, outbox_id|null, reply_to_guid|null }` `Attachment = { filename|null, mime_type|null, size|null, url|null, transcription|null }` — `transcription` is Apple's on-device transcription on inbound voice memos. ### `POST /v1/messages` Body: `{ from: , to: , text?, attachments?: [file_...], reply_to?: msg_... | }` At least one of `text` or `attachments` is required. `attachments` is at most one file ID. Send to a `cht_...` chat ID for group chats. Returns `201 { id: obx_..., status: "pending", request_id }`. Errors include `400 missing_required_parameter`, `403 contact_has_not_messaged`, `404 line_not_found`, `404 chat_not_found`, `404 file_not_found`, `404 message_not_found`. ### `POST /v1/audio-messages` Body: `{ from, to, audio_message: file_..., reply_to? }` — sends a native iMessage waveform balloon. Audio formats: m4a, mp3, wav, caf, aiff (transcoded server-side). iMessage only — SMS lines rejected. Some lines without advanced features return `400 advanced_features_required`. ### `POST /v1/reactions` Body: `{ from, to, message_id: msg_... | , type: "love"|"like"|"dislike"|"laugh"|"emphasize"|"question" }`. Returns the same outbox shape. ### `GET /v1/reactions?message_id=msg_...` List reactions on a message. `Reaction = { id: rxn_..., message_id, type, sender, is_from_me, added, sent_at, synced_at }`. ### `POST /v1/typing` Body: `{ from, to, state?: "on" | "off" }` (default `on`). ### `GET /v1/typing?from=&to=` `TypingIndicator = { id: ind_..., chat_id, handle, is_typing, updated_at }`. ### `POST /v1/receipts` Body: `{ from, to }` — marks the chat as read. ### `GET /v1/receipts?from=&to=` `ReadReceipt = { id: rcp_..., chat_id, handle, last_read_at, synced_at }`. ### `GET /v1/outbox?id=obx_...` `OutboxItem = { id, line_id, status: "pending"|"claimed"|"sent"|"failed", payload, completed_at|null, error|null, attempts, max_attempts, created_at, request_id }`. ### `POST /v1/files` Raw bytes as the body. Headers: `Content-Type: `, optional `X-Filename`. Returns `201 { id: file_..., url|null, filename|null, mime_type, size, request_id }`. ### `GET /v1/files?id=file_...` `302` redirect to the storage URL. ### `POST /v1/webhooks` Body: `{ from: , url, events: [event_name, ...] }`. Returns `201 { id: wh_..., line_ids, url, events, secret, is_active, request_id }` — the `secret` is shown once. Allowed events: `message.received`, `message.sent`, `reaction.added`, `reaction.removed`. (`typing.*` and `receipt.read` are not delivered today; subscribing to them is rejected.) ### `GET /v1/webhooks?from=` / `DELETE /v1/webhooks` (body `{ id: wh_... }`) Standard list/delete. Webhook IDs are passed in the **request body** for DELETE, not the URL. ## Recipes (capabilities that compose existing endpoints) | Capability | Mechanism | SDK | |---|---|---| | Send a contact card | `POST /v1/files` (`Content-Type: text/vcard`) → `POST /v1/messages` with the file ID in `attachments`. The SDK helper builds the vCard for you (name, phones, emails, org, photo, etc.). | `client.sendContactCard({ from, to, firstName, lastName, phones, emails, ... })` | | Send a native audio message | `POST /v1/files` (audio mime) → `POST /v1/audio-messages` with `audio_message: file_...` | `client.sendAudioMessage({ from, to, audioMessage })` | | Reply to a specific message | `reply_to: "msg_..."` (or a raw iMessage GUID) on `POST /v1/messages` | `replyTo` parameter on every send method | | Send into a group chat | Pass a `cht_...` chat ID as `to`. Discover group chats with `GET /v1/chats` (`is_group: true`). Apple does not let third-party software create new group chats — you can only send into chats that already exist on your line. | Same `to` parameter | | Test webhooks locally | `buildWebhookDelivery(event, data, secret)` returns a real signed `{ body, headers }` you can POST at your handler — `verifyWebhook` accepts it without a test-mode flag. | `buildWebhookDelivery`, `signWebhook` | ## Webhooks ### Delivery format Every event arrives as `POST ` with these headers: - `X-Webhook-Signature` — lowercase hex HMAC-SHA256 of `${timestamp}.${rawBody}` using the webhook secret - `X-Webhook-Timestamp` — Unix ms of dispatch; reject deliveries more than 5 minutes off (replay protection) - `X-Webhook-Delivery-Id` — `dlv_...` ID for idempotency - `Content-Type: application/json` Body: ```json { "event": "", "data": { /* see per-event shape below */ }, "timestamp": 1710000000123, "delivery_id": "dlv_..." } ``` The SDK helper `verifyWebhook(rawBody, headers["x-webhook-signature"], secret)` verifies the signature, performs replay-protection on the timestamp, and returns a typed discriminated-union event. ### Event payload shapes Every event includes `line_handle` (the receiving line's phone number / Apple ID) so you don't need to side-channel it. Reactions also include `chat_id` so you can route to a thread without an extra `getMessage` call. #### `message.received` and `message.sent` `data` is a `Message` plus `line_handle: string`: ```json { "id": "msg_...", "line_id": "ln_...", "line_handle": "+15551234567", "chat_id": "cht_...", "guid": "", "sender": "+15559876543", "text": "Hey!", "attachments": [], "service": "imessage", "is_from_me": false, "is_audio_message": false, "sent_at": 1710000000000, "synced_at": 1710000000001, "outbox_id": null, "reply_to_guid": null } ``` For inbound voice memos: `is_audio_message: true`, `text: null`, and `attachments[0]` carries `mime_type: "audio/x-caf"`, the audio bytes via `url`, and `transcription` (may be `null` on the first delivery if Apple hasn't finished on-device transcription yet). #### `reaction.added` and `reaction.removed` `data` is a `Reaction` plus `chat_id` and `line_handle`: ```json { "id": "rxn_...", "message_id": "msg_...", "chat_id": "cht_...", "line_handle": "+15551234567", "type": "love", "sender": "+15559876543", "is_from_me": false, "added": true, "sent_at": 1710000000000, "synced_at": 1710000000001 } ``` `added: true` for `reaction.added`, `false` for `reaction.removed`. `type` is one of `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`. ## Error envelope ```json { "error": { "type": "invalid_request_error", "code": "missing_required_parameter", "message": "...", "param": "from" }, "request_id": "req_..." } ``` Error types: `authentication_error` (401), `authorization_error` (403), `invalid_request_error` (400 typically; `contact_has_not_messaged` is 403), `not_found_error` (404), `rate_limit_error` (429). Common codes: `missing_api_key`, `invalid_api_key`, `insufficient_scope`, `line_not_accessible`, `missing_required_parameter`, `invalid_parameter_value`, `invalid_json`, `empty_file`, `advanced_features_required`, `contact_has_not_messaged`, `sandbox_not_activated`, `sandbox_contact_mismatch`, `sandbox_quota_exceeded`, `sandbox_requires_owner`, `line_not_found`, `chat_not_found`, `message_not_found`, `outbox_item_not_found`, `webhook_not_found`, `file_not_found`, `rate_limit_exceeded`.