Skip to main content

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!",
});
FieldRequiredDescription
fromYesThe line handle to send from
toYesRecipient phone number, Apple ID, or chat ID (cht_... for group chats)
textConditionalMessage text. Required unless attachments is set.
attachmentsConditionalArray of file IDs (max 1) from POST /v1/files. Required unless text is set.
reply_toNoMessage 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.

Contact-first restriction

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);
});
StatusMeaning
pendingQueued for delivery
claimedBeing processed
sentDelivered
failedDelivery failed (check the error field)

Errors

StatusCodeCause
400missing_required_parameterMissing from, to, or both text and attachments
400invalid_parameter_valueattachments is malformed (e.g. wrong ID prefix or more than 1 entry)
401missing_api_keyNo Authorization header
403insufficient_scopeKey lacks messages:write scope
403line_not_accessibleKey can’t access this line
403contact_has_not_messagedThe recipient (or chat) hasn’t messaged your line first
404line_not_foundInvalid line handle
404chat_not_foundThe cht_... ID doesn’t exist on this line
404file_not_foundAn attachment file ID doesn’t exist
404message_not_foundreply_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);
  }
}