> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://developer.fortresstech.io/api-reference/messaging/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
(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 | > Overview of the Messaging API endpoints and functionality ## API Docs - Messaging [List Conversations](https://developer.fortresstech.io/api-reference/messaging/list-conversations.md) - Messaging [List Messages](https://developer.fortresstech.io/api-reference/messaging/list-messages.md) - Messaging [Send Message](https://developer.fortresstech.io/api-reference/messaging/send-message.md) - Messaging [Read Message](https://developer.fortresstech.io/api-reference/messaging/read-message.md) - Messaging [Update Message Needs Help](https://developer.fortresstech.io/api-reference/messaging/update-message-needs-help.md) ## OpenAPI Specification The raw OpenAPI 3.1 specification for this API is available at: - [OpenAPI JSON](https://developer.fortresstech.io/openapi.json) - [OpenAPI YAML](https://developer.fortresstech.io/openapi.yaml)