# Messages

> Send SMS and MMS from your business number, and search every message body and call transcript.

Source: https://dev.ody.co/api/messages

Messages are activities inside a [conversation](https://dev.ody.co/api/conversations). Sending through the API is exactly like a teammate typing in the app: the text goes out from your business number, appears in the thread, and the reply comes back to the same thread (and to your [`message.received`](https://dev.ody.co/webhooks/events#message-received) webhook).

> **Before you can text:**
US carriers require a **10DLC registration** before a local number can send texts. Until the campaign is approved, a live `POST /messages` returns `409 failed_precondition`, and the message says exactly what's missing (not filed, in carrier review, waiting for a PIN, or rejected). Check `canText` with [`GET /messaging`](https://dev.ody.co/api/messaging#retrieve-registration-status). Some newly filed numbers can text earlier because Ody temporarily attaches them to its own approved campaign while yours is reviewed. Test keys skip this gate, since nothing is sent.

**`POST /v1/api/messages`**

### Send a message

Sends an SMS, or an MMS when you include `mediaUrls`. Address it in one of two ways:

- **`conversationId`**: reply in an existing thread, from that thread's number.
- **`to`**: an E.164 number. Ody finds or creates the thread from your workspace number, or from `from` if you own that number.

Delivery is asynchronous. The response returns once the carrier has accepted the message; the final outcome arrives as [`message.delivered`](https://dev.ody.co/webhooks/events#message-delivered) or [`message.failed`](https://dev.ody.co/webhooks/events#message-failed).

With a **test key** (`ody_test_…`) the message is recorded with `deliveryStatus: "simulated"` and nothing is sent.

**Scope:** `messages:send` · supports [`Idempotency-Key`](https://dev.ody.co/api/idempotency)

**Body**

- `body` *string* **required** — The text. Longer messages are split into segments by the carrier.
- `conversationId` *uuid* — Reply in this thread. Use this or `to`.
- `to` *string* — Recipient in E.164. Use this or `conversationId`.
- `from` *string* — One of your workspace numbers, used when starting a thread with `to`. Ignored for `conversationId` (a reply always goes out from the thread's number). Defaults to your workspace number.
- `mediaUrls` *array of URLs* — Up to 10 publicly fetchable images or files, sent as MMS.

**Returns** `201` with `{ message: Activity, conversationId, simulated? }`.

**Errors:** `409` when the workspace has no active number or texting isn't registered yet; `404` for an unknown `conversationId`.

cURL:

```bash
curl https://api.ody.co/v1/api/messages \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appt-7731-reminder" \
  -d '{
    "to": "+15125550123",
    "body": "Hi Ada, reminder: your appointment is tomorrow at 10am. Reply C to confirm."
  }'
```
TypeScript:

```ts
const { message, conversationId } = await ody.messages.send({
  to: "+15125550123",
  body: "Hi Ada, reminder: your appointment is tomorrow at 10am. Reply C to confirm.",
  idempotencyKey: "appt-7731-reminder",
});
```
Python:

```python
res = client.messages.send(
    to="+15125550123",
    body="Hi Ada, reminder: your appointment is tomorrow at 10am. Reply C to confirm.",
    idempotency_key="appt-7731-reminder",
)
```

Response:

```json
{
  "message": {
    "id": "5e7f…",
    "type": "message",
    "direction": "outbound",
    "body": "Hi Ada, reminder: your appointment is tomorrow at 10am. Reply C to confirm.",
    "media": [],
    "deliveryStatus": "queued",
    "createdAt": "2026-10-14T18:10:00.000Z"
  },
  "conversationId": "c4f1a2b3-5d6e-4f70-8a9b-0c1d2e3f4a5b"
}
```

**`GET /v1/api/messages/search`**

### Search messages

Case-insensitive search across message bodies **and** stored call and voicemail transcripts, newest first. It's the same retrieval the MCP [`search_messages`](https://dev.ody.co/mcp/tools#search_messages) tool uses.

**Scope:** `conversations:read`

**Query parameters**

- `q` *string* **required** — Text to find.
- `limit` *integer* — 1–100, default 25.

**Returns** `{ results: [{ activityId, conversationId, type, direction, body, participants, numberE164, createdAt }] }`.

cURL:

```bash
curl "https://api.ody.co/v1/api/messages/search?q=refund&limit=10" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const results = await ody.messages.search("refund", { limit: 10 });
```
Python:

```python
results = client.messages.search("refund", limit=10)
```
