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

# Messaging Overview

> Overview of the Messaging API endpoints and functionality

> **Note**
>
> The Messaging API is available by request. To explore availability for your integration, please contact our team.

Use the Messaging API to send SMS messages to customers and prospects, track delivery, and read the
conversation history a property has with a resident or applicant.

## Key Concepts

### Conversations and Messages

A **conversation** is the running thread between one property and one recipient. A **message** is a single
SMS within that thread. You never create a conversation directly — Fortress opens one the first time a
property and a recipient exchange a message, and every message after that is appended to it.

A conversation follows the person, not their record. Someone who texts a property as a prospect and later
becomes a resident keeps the same conversation, which is why `recipients` can hold both a `prospectId` and a
`customerId` over the thread's lifetime. The `recipient` object identifies the party currently being messaged.

### Recipients

Every send targets either a `customerId` or a `prospectId`. Fortress resolves that id to a phone number and to
the property phone number the message should come from, so you never supply phone numbers yourself.

### Idempotency

`idempotencyId` is required on every send and must uniquely identify the request. If a send is retried with
the same key — after a network timeout, for example — Fortress returns the message created by the original
request rather than sending the recipient a duplicate.

Choose keys that are stable across retries of the same logical send and distinct across different sends. A
UUID generated once, before the first attempt, is the usual choice.

You can also look a message up by its key later with `GET /messages?idempotencyId=...`, which is the reliable
way to determine whether a send you lost the response to was actually recorded.

### Delivery Is Asynchronous

A `201` from `POST /messages` means Fortress accepted the message and handed it to the carrier. It does not
mean the recipient received it.

The returned `messageStatus` is `PROCESSING` while delivery is in flight. Poll `GET /messages/{messageId}`
until it reaches one of the terminal statuses:

| Status        | Meaning                                                            |
| ------------- | ------------------------------------------------------------------ |
| `DELIVERED`   | The carrier confirmed delivery to the handset.                     |
| `UNDELIVERED` | The carrier accepted the message but could not deliver it.         |
| `FAILED`      | The message could not be sent. `messageStatusReason` explains why. |

Statuses reported by the carrier are passed through verbatim, so treat `messageStatus` as an open set of
strings rather than a fixed enumeration, and match on the terminal values above rather than assuming the
intermediate ones.

### Message Limits

`content` is limited to **1600 characters**. Longer messages are rejected with a `400`.

Attachment `url` values on a message are generated per request and expire **15 minutes** after the response is
issued. Download promptly, or re-read the message to get a fresh URL.

### Needs Help

A **needs-help flag** marks a single message as requiring a human to look at it — Leslie, our AI agent, raises
one when unable to answer a resident's question, and a Fortress staff member can raise or resolve one too.
`PATCH /messages/{messageId}/needsHelp` handles both directions: `isFlagged: true` raises the flag,
`isFlagged: false` resolves it.

This is a different signal from `conversationStatus: UNRESOLVED`. `UNRESOLVED` is thread-level and set by a
human working the conversation as a whole; a needs-help flag is per-message and is typically raised by the
agent itself, at the exact point it gave up on a specific question. The two coexist: a conversation can be
`UNRESOLVED` with no flags, or flagged while the underlying workflow is anywhere in its own lifecycle.

While any message on a conversation has an open flag, `GET /conversations` and `GET /messages/{messageId}`
report `conversationStatus: NEEDS_HELP` regardless of what the workflow status underneath it is doing.
`NEEDS_HELP` **prevails**: it does not clear just because a new message arrives or a bot closes the thread.
Only resolving every open flag clears it, and when that happens the conversation falls through to whatever
status its current workflow calls for — not whatever status was in effect when the first flag was raised.

The conversation payload carries the ids of every currently-flagged message under `needsHelpMessageIds`, so
you can jump straight to the messages that need attention without scanning the whole thread:

```json
{
  "id": "68a1f4c29b3e7a0012d4e87f",
  "conversationStatus": "NEEDS_HELP",
  "needsHelpMessageIds": ["68a1f4c29b3e7a0012d4e8a3"]
}
```

`needsHelpMessageIds` is empty whenever nothing is flagged. On the message itself, `GET /messages/{messageId}`
returns `needsHelp` as a plain boolean alongside a `needsHelpDetails` object holding the reason, who flagged it
and when, and who resolved it and when. `needsHelpDetails` outlives the flag: after a resolution `needsHelp`
returns to `false` but the details still describe who asked for help and why.

```bash
curl --location --request PATCH 'https://api.fortresstech.io/v1/messages/68a1f4c29b3e7a0012d4e8a3/needsHelp' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
    "isFlagged": true,
    "reason": "Resident asked about a lease clause that needs a human to verify"
}'
```

The request is idempotent: flagging an already-flagged message, or resolving one that isn't flagged, returns
`200` with the current state rather than an error.

## Permissions

Messaging spans two permission scopes, `messages` and `conversations`, which are granted independently on your
API key. A key that can send messages cannot necessarily list conversations. Request both if your integration
needs the full surface.

Within the `messages` scope, the grant is by action rather than by endpoint. Flagging or resolving a
needs-help flag requires the `update` action — a key row needs `messages: ['update']`, or the request 403s
before it reaches the backend. That grant is **service-wide**: it authorizes every write under
`/v1/messages/*`, not only this endpoint, so a key with `messages: ['update']` today also gets any future
`PUT`/`PATCH` added under that path.

## Sending a Message

```mermaid
sequenceDiagram
    participant Client as Your Integration
    participant API as Fortress API
    participant Carrier as SMS Carrier
    participant Resident

    Note over Client,Resident: Step 1: Send
    Client->>API: POST /messages<br />(customerId, content, idempotencyId)
    API->>Carrier: Hand off for delivery
    API-->>Client: 201 { id, messageStatus: "PROCESSING" }

    Note over Client,Resident: Step 2: Delivery
    Carrier->>Resident: SMS
    Carrier-->>API: Delivery receipt
    Note over API: messageStatus -> DELIVERED

    Note over Client,Resident: Step 3: Poll for status
    Client->>API: GET /messages/{messageId}
    API-->>Client: 200 { messageStatus: "DELIVERED" }

    Note over Client,Resident: Step 4: Resident replies
    Resident->>Carrier: SMS reply
    Carrier-->>API: Inbound message
    Note over API: conversationStatus -> NEW

    Client->>API: GET /messages?conversationId={id}
    API-->>Client: 200 [ ...thread including the reply ]
```

### Step 1: Send the Message

```bash
curl --location 'https://api.fortresstech.io/v1/messages' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
    "customerId": "6e46a1d7-503a-42fa-a55b-047ee04b5279",
    "content": "Your maintenance request has been scheduled for Thursday between 9am and noon.",
    "idempotencyId": "049665d5-2a23-4f37-9c5e-da5dd30f5364"
}'
```

The response is the created message:

```json
{
  "id": "68a1f4c29b3e7a0012d4e881",
  "conversationId": "68a1f4c29b3e7a0012d4e87f",
  "type": "OUTBOUND",
  "content": "Your maintenance request has been scheduled for Thursday between 9am and noon.",
  "messageStatus": "PROCESSING",
  "messageSid": "SM7f3a1c2d4e5b6a7089c1d2e3f4a5b6c7",
  "idempotencyId": "049665d5-2a23-4f37-9c5e-da5dd30f5364",
  "customerId": "6e46a1d7-503a-42fa-a55b-047ee04b5279",
  "organizationId": "123e4567-e89b-12d3-a456-426614174000",
  "metadata": {
    "sentAt": "2026-08-02T14:32:07.184Z",
    "sentBy": {
      "id": "9c8b7a6d-5e4f-4321-8765-0fedcba98765",
      "name": "Dana Whitfield"
    }
  },
  "createdAt": "2026-08-02T14:32:07.184Z",
  "updatedAt": "2026-08-02T14:32:07.184Z"
}
```

**Important Notes:**

* Supply exactly one of `customerId` or `prospectId`. Supplying both is not rejected: `customerId` takes
  precedence and the `prospectId` you sent is ignored.
* `messageSid` is the carrier's identifier. It is absent until the carrier accepts the message.
* `metadata.sentBy` records the Fortress user the message was sent on behalf of. Any `metadata` you supply is
  preserved alongside it and returned unchanged on reads.

### Step 2: Check Delivery Status

```bash
curl --location 'https://api.fortresstech.io/v1/messages/68a1f4c29b3e7a0012d4e881' \
--header 'x-api-key: YOUR_API_KEY'
```

When delivery fails, `messageStatusReason` carries the explanation:

```json
{
  "id": "68a1f4c29b3e7a0012d4e881",
  "messageStatus": "FAILED",
  "messageStatusReason": "The 'To' number is not a valid mobile number",
  "updatedAt": "2026-08-02T14:32:11.902Z"
}
```

### Step 3: Read the Conversation

To see the full thread, list the conversations for the recipient and then read the messages on the
conversation you want:

```bash
curl --location 'https://api.fortresstech.io/v1/conversations?recipientId=6e46a1d7-503a-42fa-a55b-047ee04b5279' \
--header 'x-api-key: YOUR_API_KEY'
```

```json
[
  {
    "id": "68a1f4c29b3e7a0012d4e87f",
    "conversationStatus": "NEW",
    "messageType": "SMS",
    "propertyId": "123e4567-e89b-12d3-a456-426614174000",
    "recipient": {
      "customerId": "6e46a1d7-503a-42fa-a55b-047ee04b5279",
      "lastUsedPhoneNumberId": "68a1f4c29b3e7a0012d4e7c1"
    },
    "recipients": [
      { "customerId": "6e46a1d7-503a-42fa-a55b-047ee04b5279" }
    ],
    "lastReceivedMessageId": "68a1f4c29b3e7a0012d4e8a3",
    "lastReceivedMessage": {
      "id": "68a1f4c29b3e7a0012d4e8a3",
      "type": "INBOUND",
      "content": "Thursday morning works, thanks!",
      "messageStatus": "RECEIVED",
      "createdAt": "2026-08-02T15:07:44.310Z"
    },
    "createdAt": "2026-07-28T18:02:15.007Z",
    "updatedAt": "2026-08-02T15:07:44.310Z"
  }
]
```

```bash
curl --location 'https://api.fortresstech.io/v1/messages?conversationId=68a1f4c29b3e7a0012d4e87f' \
--header 'x-api-key: YOUR_API_KEY'
```

A `conversationStatus` of `NEW` means an inbound message is waiting on a response. `UNRESOLVED` means someone
is working the thread, and `RESOLVED` means it has been closed.

### Confirming a Send You Lost the Response To

If a send times out and you are unsure whether it was recorded, look it up by the key you used rather than
retrying blindly:

```bash
curl --location 'https://api.fortresstech.io/v1/messages?idempotencyId=049665d5-2a23-4f37-9c5e-da5dd30f5364' \
--header 'x-api-key: YOUR_API_KEY'
```

An empty array means the send never landed and is safe to reissue. Retrying with the same `idempotencyId` is
also safe — it returns the original message instead of sending a second one.

## Available Endpoints

| Endpoint                                                                                | Summary                   | Method |
| --------------------------------------------------------------------------------------- | ------------------------- | :----: |
| [`/conversations`](/api-reference/messaging/list-conversations)                         | List Conversations        |   GET  |
| [`/messages`](/api-reference/messaging/list-messages)                                   | List Messages             |   GET  |
| [`/messages/{messageId}`](/api-reference/messaging/read-message)                        | Read Message              |   GET  |
| [`/messages`](/api-reference/messaging/send-message)                                    | Send Message              |  POST  |
| [`/messages/{messageId}/needsHelp`](/api-reference/messaging/update-message-needs-help) | Update Message Needs Help |  PATCH |