Messaging Overview
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:
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:
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.
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
Step 1: Send the Message
The response is the created message:
Important Notes:
- Supply exactly one of
customerIdorprospectId. Supplying both is not rejected:customerIdtakes precedence and theprospectIdyou sent is ignored. messageSidis the carrier’s identifier. It is absent until the carrier accepts the message.metadata.sentByrecords the Fortress user the message was sent on behalf of. Anymetadatayou supply is preserved alongside it and returned unchanged on reads.
Step 2: Check Delivery Status
When delivery fails, messageStatusReason carries the explanation:
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:
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:
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.

