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

# Reply to a message

POST https://api.getdial.ai/api/v1/messages/{messageId}/reply
Content-Type: application/json

Sends a threaded reply or a reaction targeted at an existing message.

**The sender is derived from the target message.** The reply goes out
from the Dial number the target message belongs to, to the other party
of that message — there is no `to` or `fromNumberId`. A reply always
stays in the conversation the target message is part of.

The request carries **exactly one** of:

- `body` — a text reply. On iMessage numbers it is delivered as a
  native threaded reply (quoting the target message) when the recipient
  supports threads, and as a regular message otherwise. On WhatsApp
  numbers it is delivered as a native quoted reply (the target message
  is quoted above the reply). On standard numbers it is delivered as a
  regular SMS.
- `reaction` — a reaction to the target message: one of the six
  reaction names (`love`, `like`, `dislike`, `laugh`, `emphasize`,
  `question`) or a **single emoji** (one visible symbol — skin-tone and
  multi-person emoji count as one; anything else is rejected with
  `400`). On iMessage numbers the reaction is delivered natively (a
  Tapback). When the recipient can only receive SMS — including
  everything sent from standard numbers — an **emoji** reaction is
  delivered as a regular message whose body is the emoji, and a
  reaction **name** is rejected with `400` (names have no SMS
  rendering; send an emoji instead). On **WhatsApp** numbers the
  reaction is delivered natively too, and a reaction **name** is mapped
  to its emoji — 👍 `like`, ❤️ `love`, 👎 `dislike`, 😂 `laugh`,
  ‼️ `emphasize`, ❓ `question` — so both a name and a single emoji
  react natively.

**Targets.** Any message on the account can be targeted, with one
current restriction: on iMessage numbers the target must be an
**inbound** message — replying to your own sent messages isn't
supported on those numbers yet. Standard and WhatsApp numbers accept
targets in both directions. On **WhatsApp** numbers the target may be a
**group** message — the reply or reaction is then delivered to the
group; on other channels a group message cannot be the target and is
rejected with `400`. A **reaction cannot itself be the target** — replying
or reacting to a reaction is rejected with `400`.

**A `body` reply on an iMessage number also needs a threadable
target.** A threaded reply attaches to the target by the message id its
channel carries, and only messages delivered over iMessage or RCS carry
one. A target that arrived over **SMS** — the fallback used when the
other party has neither — has nothing to attach to, so a `body` reply
to it is rejected with `400`. Send a new message instead
(`POST /api/v1/messages`): the same conversation takes it over SMS, it
just isn't quoted on the recipient's device. A `reaction` on such a
target is unaffected and follows the reaction rules above (an emoji is
delivered as a regular message; a reaction name is rejected with
`400`).

**On WhatsApp numbers only a `reaction` is supported, not a `body`
reply, for now** — a `body` reply to a WhatsApp message is rejected with
`400`. Send a new message in the same conversation instead
(`POST /api/v1/messages`); the `reaction` rules above apply and are
delivered natively.

The created message is recorded like any outbound message and billed
the same as Send a message. Its `replyToId` points at the target
message; `reaction` carries the reaction string when one was sent (a
reaction delivered natively has an empty `body`; one delivered as a
regular message carries the emoji in `body` too).

Not idempotent — retrying a failed request can send a duplicate reply.
On an ambiguous failure, confirm via List messages before re-sending.


Reference: https://docs.getdial.ai/api-reference/rest-api/messages/reply-to-message

## Authentication

- `Authorization` header (bearer token, required) — Your Dial API key, sent as `Authorization: Bearer sk_live_...`

## Request

### Path parameters

- `messageId` (string, required) — ID of the message to reply or react to (see List messages).

### Body (application/json)

- `body` (string, optional) — Reply text. Exactly one of `body` and `reaction` is required. On a standard number the same content-size limit as Send a message applies (1530 characters, or 670 if the text uses characters outside the basic SMS alphabet); a longer reply is rejected with `400`.
- `reaction` (string, optional) — Reaction to send: `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`, or a single emoji. Exactly one of `body` and `reaction` is required.

## Response

### 201

Reply queued. The created message, with `replyToId` set to the target message's ID.

- `message` (object, optional)
  - `id` (string, optional)
  - `phoneNumberId` (string, optional)
  - `from` (string, optional) — Sender in E.164 format. On an inbound group message this is the participant who sent it — not the group, and not your own line.
  - `to` (string, optional, nullable) — Recipient in E.164 format, and **null exactly when `groupId` is set** — in both directions. A group message is addressed to the group, and a group is not a phone number. Which of your numbers the conversation is on is `phoneNumberId`, which is set on every message. Code that reads `to` as "my number" must read `phoneNumberId` instead.
  - `groupId` (string, optional, nullable) — The group conversation this message belongs to, or null for a one-to-one conversation. A Dial ID (see List groups) — never the channel's own group identifier.
  - `body` (string, optional)
  - `direction` (enum, optional)
    - Allowed values: `inbound`, `outbound`
  - `channel` (enum, optional) — The channel the message was delivered on. `sms` for SMS/call numbers. For iMessage numbers, inbound messages report the channel actually used — `imessage`, `rcs`, or `sms`. Outbound iMessage sends report `unknown`, because the iMessage channel does not confirm which channel was ultimately used. `whatsapp` is a channel in its own right — a WhatsApp registration on an iMessage line — and is reported in both directions. Every group message is `whatsapp` today.
    - Allowed values: `sms`, `imessage`, `rcs`, `whatsapp`, `unknown`
  - `service` (enum, optional) — For iMessage numbers, the confirmed delivery channel. Null for SMS/call numbers (where `channel` already says `sms`). Deprecated — prefer `channel`, which is authoritative.
    - Allowed values: `imessage`, `rcs`, `sms`
  - `status` (enum, optional) — Delivery status. Outbound SMS/call messages start at `sent` and reach a terminal `delivered`, `undelivered`, or `failed`. Outbound iMessage messages are `unknown` (the iMessage channel does not report delivery). Inbound messages are `received`.
    - Allowed values: `sent`, `delivered`, `undelivered`, `failed`, `unknown`, `received`
  - `statusError` (string, optional, nullable) — A human-readable reason, present only when `status` is `undelivered` or `failed` (for example, when a carrier rejects the message). Null otherwise.
  - `deliveryState` (enum, optional) — The delivery axis — the same information as `status`, in the vocabulary that says what to do about it. `pending` means wait for a disposition (`status: "sent"`); `unconfirmed` means never expect one (`status: "unknown"`, an iMessage send, or a WhatsApp send before its first receipt). Inbound messages are `delivered`.
    - Allowed values: `pending`, `delivered`, `undelivered`, `failed`, `unconfirmed`
  - `readState` (enum, optional) — The read axis, independent of delivery — a message can be both delivered and read, which `status` alone could never express. `unsupported` on channels that report no reads (SMS) and on every inbound message, since Dial does not track whether you read one. iMessage and WhatsApp report reads, so an outbound message there is `unread` until it is `read`. In a group, `read` means every recipient has read it — never just somebody.
    - Allowed values: `unread`, `read`, `unsupported`
  - `readAt` (datetime, optional, nullable) — When the message was read, or null. Always null on inbound messages and on channels that report no reads.
  - `deliveryError` (string, optional, nullable) — The current name for `statusError`; identical value. A vendor-neutral reason, present only on an `undelivered` or `failed` message.
  - `media` (list of object, optional) — Media attachments on the message, in send order. Empty for plain text messages.
    - `id` (string, optional) — Public media ID — an unguessable 32-character token.
    - `url` (string, optional) — Stable public URL serving the media (see Get public media). Safe to use directly as an image source; requires no authentication.
    - `contentType` (string, optional) — MIME type of the media.
    - `originalUrl` (string, optional, nullable) — The caller-supplied source URL on outbound messages. Null on inbound messages and when the media was uploaded directly as bytes — inbound media is always served from `url`.
  - `replyToId` (string, optional, nullable) — ID of the message this one replies or reacts to. Set on messages created via Reply to a message, and on inbound threaded replies and reactions received on iMessage numbers. Null for ordinary messages, or when the target of an inbound reply isn't a message on your account.
  - `reaction` (string, optional, nullable) — The reaction this message carries — a reaction name (`love`, `like`, `dislike`, `laugh`, `emphasize`, `question`) or an emoji — when the message is a reaction, sent or received. Null otherwise. A reaction delivered natively has an empty `body`; a reaction delivered as a regular message over SMS carries the emoji in `body` too.
  - `createdAt` (datetime, optional)

## Examples

**Request**

```json
{}
```

**Response**

```json
{
  "message": {
    "id": "string",
    "phoneNumberId": "string",
    "from": "+14155550123",
    "to": "+14155559876",
    "groupId": null,
    "body": "string",
    "direction": "inbound",
    "channel": "sms",
    "service": "imessage",
    "status": "delivered",
    "statusError": null,
    "deliveryState": "delivered",
    "readState": "unread",
    "readAt": null,
    "deliveryError": null,
    "media": [
      {
        "id": "a3f9c2d41e8b4f0a9c6d2e7b5a1f8c30",
        "url": "https://getdial.ai/public-media/a3f9c2d41e8b4f0a9c6d2e7b5a1f8c30.jpg",
        "contentType": "image/jpeg",
        "originalUrl": "https://your-cdn.example.com/summary.png"
      }
    ],
    "replyToId": null,
    "reaction": null,
    "createdAt": "2024-01-15T09:30:00Z"
  }
}
```

**SDK Code**

```python
import requests

url = "https://api.getdial.ai/api/v1/messages/messageId/reply"

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

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

print(response.json())
```

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

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.getdial.ai/api/v1/messages/messageId/reply"

	payload := strings.NewReader("{}")

	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.getdial.ai/api/v1/messages/messageId/reply")

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 = "{}"

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.getdial.ai/api/v1/messages/messageId/reply")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

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

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

```csharp
using RestSharp;

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

```swift
import Foundation

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

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

let request = NSMutableURLRequest(url: NSURL(string: "https://api.getdial.ai/api/v1/messages/messageId/reply")! 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()
```