Documentation Index
Fetch the complete documentation index at: https://messages.dev/docs/llms.txt
Use this file to discover all available pages before exploring further.
Send a message
import { createClient } from "@messages-dev/sdk";
const client = createClient();
await client.sendMessage({
from: "+15551234567",
to: "+15559876543",
text: "Hello from Messages.dev!",
});
| Field | Required | Description |
|---|
from | Yes | The line handle to send from |
to | Yes | Recipient phone number, Apple ID, or chat ID (cht_... for group chats) |
text | Conditional | Message text. Required unless attachments is set. |
attachments | Conditional | Array of file IDs (max 1) from POST /v1/files. Required unless text is set. |
reply_to | No | Message ID (msg_...) or iMessage GUID to thread the message as a reply. |
Reply to a specific message
Pass the original message’s id or guid as replyTo to thread your reply
underneath it in the recipient’s conversation:
await client.sendMessage({
from: "+15551234567",
to: "+15559876543",
text: "Got it!",
replyTo: "msg_abc123",
});
Group chats
Pass a chat ID (cht_...) returned by listChats
as to:
await client.sendMessage({
from: "+15551234567",
to: "cht_abc123",
text: "Hey team!",
});
The contact-first rule below applies to group chats too — the chat must have
at least one inbound message before you can send into it. See
Group chats for the full flow.
To keep lines healthy with Apple’s spam detection, you can only send to a
contact (or group chat) after they have messaged your line first. Trying
to send first returns:
{
"error": {
"type": "invalid_request_error",
"code": "contact_has_not_messaged",
"message": "Cannot send to a contact who has not messaged this line first.",
"param": "to"
}
}
The sandbox line is exempt once activated — your paired phone number is
treated as having messaged in. For production lines, drive inbound traffic
via marketing channels (a tap-to-text link, a vCard with your line saved as a
contact, etc.) before you can reply.
Track delivery
Register a webhook for message.sent and your server will receive a POST when
the message is delivered:
import { verifyWebhook } from "@messages-dev/sdk";
app.post("/webhooks", async (req, res) => {
const event = await verifyWebhook(
req.body,
req.headers["x-webhook-signature"],
"your_webhook_secret",
);
if (event.event === "message.sent") {
console.log(`Delivered: ${event.data.id}`);
}
res.sendStatus(200);
});
| Status | Meaning |
|---|
pending | Queued for delivery |
claimed | Being processed |
sent | Delivered |
failed | Delivery failed (check the error field) |
Errors
| Status | Code | Cause |
|---|
| 400 | missing_required_parameter | Missing from, to, or both text and attachments |
| 400 | invalid_parameter_value | attachments is malformed (e.g. wrong ID prefix or more than 1 entry) |
| 401 | missing_api_key | No Authorization header |
| 403 | insufficient_scope | Key lacks messages:write scope |
| 403 | line_not_accessible | Key can’t access this line |
| 403 | contact_has_not_messaged | The recipient (or chat) hasn’t messaged your line first |
| 404 | line_not_found | Invalid line handle |
| 404 | chat_not_found | The cht_... ID doesn’t exist on this line |
| 404 | file_not_found | An attachment file ID doesn’t exist |
| 404 | message_not_found | reply_to references a message that doesn’t exist |
| With the SDK, errors are thrown as typed exceptions: | | |
import { createClient, InvalidRequestError } from "@messages-dev/sdk";
try {
await client.sendMessage({ from: "+15551234567", to: "+1555...", text: "Hi" });
} catch (err) {
if (err instanceof InvalidRequestError) {
console.error(err.code, err.param);
}
}