> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://agentphone.to/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://agentphone.to/docs/_mcp/server.

# Send Message

POST https://api.agentphone.ai/v1/messages
Content-Type: application/json

Send a message through the selected number's transport. Use channel="whatsapp" for a WhatsApp connection; see the WhatsApp guide for its 24-hour window and template requirements. Enabled branded RCS senders have separate onboarding and content rules; see /documentation/guides/rcs.

Use media_urls for public HTTPS attachments, not both media_url and media_urls. WhatsApp and group sends accept one attachment; other eligible transports accept up to 20. Use an empty body for media-only messages. Some iMessage lines reject text with multi-image carousels; native iMessage voice memos require one audio file and no text. Branded RCS cannot mix body text with media_urls; see [RCS rich cards](/docs/documentation/guides/rcs#rich-cards) for the HTTP payload.

Use to_number for an existing recipient/group, or recipients to create a supported group. Unsupported feature/number combinations return errors. A send response is not proof of delivery; inspect message history for status, failureReason, and deliveredChannel. See /documentation/guides/messages.

Reference: https://agentphone.to/docs/api-reference/messages/send-message-v-1-messages-post

## Authentication

- `Authorization` header (bearer token, required) — Bearer authentication of the form `Bearer <token>`, where token is your auth token.

## Request

### Body (application/json)

This endpoint expects a SendMessageRequest.

- `body` (string, required) — Message text. Use an empty string for media-only sends. Text plus media is transport-dependent: voice memos, some iMessage carousels, and branded RCS media messages cannot include body text.
- `agent_id` (string, optional, nullable) — The ID of the agent sending the message. Determines which phone number and provider to use.
- `to_number` (string, optional, nullable) — Recipient phone/address or an existing group identifier. Preserve group identifiers returned by the API. Supply to_number or recipients, not both.
- `recipients` (list of string, optional, nullable) — Create a new group on a supported number. Use 2–32 distinct recipients on eligible iMessage lines, or 2–10 phone numbers on supported carrier MMS lines. Mutually exclusive with to_number; response to_number is the group address for later sends. Group sends accept at most one attachment. WhatsApp and branded RCS groups are unsupported.
- `media_url` (string, optional, nullable) — Single public HTTPS attachment URL. Prefer media_urls and do not supply both fields.
- `media_urls` (list of string, optional, nullable) — Public HTTPS attachments, 1–20 on eligible transports. WhatsApp and groups accept one. Some iMessage lines reject text alongside multiple images. Native iMessage voice memos require one audio file and an empty body. Branded RCS media sends accept one file and no body text; see [RCS rich cards](/docs/documentation/guides/rcs#rich-cards) for an image with text.
- `number_id` (string, optional, nullable) — Select an attached sending number explicitly. When omitted, an eligible number is chosen automatically; attachment order is not a default-number guarantee.
- `from_number` (string, optional, nullable)
- `channel` (enum, optional) — Optional transport override for numbers connected to multiple channels. Pass "whatsapp" to send via the number's WhatsApp connection instead of its default SMS/iMessage transport. WhatsApp-only numbers use WhatsApp automatically.
  - Allowed values: `whatsapp`
- `send_style` (enum, optional) — Optional iMessage expressive effect. Only valid on iMessage-capable numbers. Supported values: celebration, fireworks, lasers, love, confetti, balloons, spotlight, echo, invisible, gentle, loud, slam.
  - Allowed values: `celebration`, `fireworks`, `lasers`, `love`, `confetti`, `balloons`, `spotlight`, `echo`, `invisible`, `gentle`, `loud`, `slam`
- `reply_to_message_id` (string, optional, nullable) — Optional parent message ID (AgentPhone Message.id) to send this message as an inline reply. Supported on iMessage and WhatsApp; the parent must be in the same chat and sent from the same number.
- `buttons` (list of string, optional, nullable) — WhatsApp only: up to 3 tappable reply buttons (each max 20 chars). The tapped button's text arrives as a normal inbound message. Pass media_urls alongside buttons to render that image, video, or document as a header above the text, in the same bubble as the buttons (audio has no header form and is rejected).
- `list` (SendMessageRequestList, optional, nullable) — WhatsApp only: a tap-to-open list menu. \{"button": "View options", "options": \["Small", "Medium", "Large"]} (max 10 options, 24 chars each), or full form with sections: \{"button": str, "sections": \[\{"title": str, "rows": \[\{"id": str, "title": str, "description": str}]}]}.
- `cta` (SendMessageRequestCta, optional, nullable) — WhatsApp only: a single tappable link button on an ordinary message. \{"display\_text": "Track order", "url": "[https://example.com/t/123](https://example.com/t/123)", "footer": "Tap to see live status"} — `footer` is optional. The url must be absolute and https. Unlike a URL button on a template this needs no Meta review, but as an ordinary message it only reaches someone inside the 24h window.
- `template` (SendMessageRequestTemplate, optional, nullable) — WhatsApp only: send a Meta-approved message template. This is the only way to reach someone outside the 24h customer service window. \{"name": "order\_shipped", "language": "en\_US", "variables": \["1234"]} — `variables` fill the template body, and `header_variables` does the same for a text header. Pass an array for a POSITIONAL template (\{\{1}}, \{\{2}}) or an object for a NAMED one, e.g. \{"variables": \{"first\_name": "Pablo"}} for a body reading "Thanks \{\{first\_name}}!". The shape must match how the template was authored. `language` defaults to en\_US. The template must already be APPROVED (see the templates endpoints). `body` is NOT delivered for template sends — WhatsApp renders the approved template content — it is kept only as a fallback label if we can't render the template for your message history.

## Response

### 200

Successful Response

- `id` (string, required)
- `status` (string, required)
- `channel` (string, required)
- `from_number` (string, required)
- `to_number` (string, required)
- `conversation_id` (string, optional, nullable)
- `media_urls` (list of string, optional, default: [])
- `reply_to_message_id` (string, optional, nullable)
- `reply_parent_unresolved` (boolean, optional, nullable)

## Errors

### 422 Unprocessable Entity Error

Validation Error

- `detail` (list of ValidationError, optional)

## Types

### SendMessageRequestList

WhatsApp only: a tap-to-open list menu. \{"button": "View options", "options": \["Small", "Medium", "Large"]} (max 10 options, 24 chars each), or full form with sections: \{"button": str, "sections": \[\{"title": str, "rows": \[\{"id": str, "title": str, "description": str}]}]}.

### SendMessageRequestCta

WhatsApp only: a single tappable link button on an ordinary message. \{"display\_text": "Track order", "url": "[https://example.com/t/123](https://example.com/t/123)", "footer": "Tap to see live status"} — `footer` is optional. The url must be absolute and https. Unlike a URL button on a template this needs no Meta review, but as an ordinary message it only reaches someone inside the 24h window.

### SendMessageRequestTemplate

WhatsApp only: send a Meta-approved message template. This is the only way to reach someone outside the 24h customer service window. \{"name": "order\_shipped", "language": "en\_US", "variables": \["1234"]} — `variables` fill the template body, and `header_variables` does the same for a text header. Pass an array for a POSITIONAL template (\{\{1}}, \{\{2}}) or an object for a NAMED one, e.g. \{"variables": \{"first\_name": "Pablo"}} for a body reading "Thanks \{\{first\_name}}!". The shape must match how the template was authored. `language` defaults to en\_US. The template must already be APPROVED (see the templates endpoints). `body` is NOT delivered for template sends — WhatsApp renders the approved template content — it is kept only as a fallback label if we can't render the template for your message history.

### ValidationError

- `loc` (list of ValidationErrorLocItems, required)
- `msg` (string, required)
- `type` (string, required)

### ValidationErrorLocItems

- string
- integer

## Examples

**Request**

```json
{
  "body": "string"
}
```

**Response**

```json
{
  "id": "string",
  "status": "string",
  "channel": "string",
  "from_number": "string",
  "to_number": "string",
  "conversation_id": "string",
  "media_urls": [
    "string"
  ],
  "reply_to_message_id": "string",
  "reply_parent_unresolved": true
}
```

**SDK Code**

```python
import requests

url = "https://api.agentphone.ai/v1/messages"

payload = { "body": "string" }
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.agentphone.ai/v1/messages';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"body":"string"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.agentphone.ai/v1/messages"

	payload := strings.NewReader("{\n  \"body\": \"string\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.agentphone.ai/v1/messages")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"body\": \"string\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.agentphone.ai/v1/messages")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"body\": \"string\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.agentphone.ai/v1/messages', [
  'body' => '{
  "body": "string"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.agentphone.ai/v1/messages");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"body\": \"string\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = ["body": "string"] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.agentphone.ai/v1/messages")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```