Receiving SMS
An inbound SMS arrives as a webhook from whichever provider carries the number, resolves to exactly one workspace, is deduplicated, attached to a person and written as an ordinary message the agent loop reads. That half of the channel works. The other half does not: sending depends entirely on the configured provider, and the carrier on the live account carries no SMS at all, which is why SMS is a foundation channel.
Where an inbound message lands#
A text message that reaches Connect stops being a text message almost immediately. sms.py normalises the provider's payload into the same canonical shape an email or a WhatsApp message takes, so by the time anything downstream sees it there is a conversation, a message on that conversation, and a contact the message is attributed to. The engine never reads the provider-shaped row, which is the reason a different SMS provider changes nothing about how a message is triaged, drafted against, or shown on a relationship timeline.
You see it on Conversations alongside every other channel. The SMS · DLT screen at #/dlt is about registration paperwork, not about reading messages, and the Phone screen carries the channel's readiness rather than its traffic.
What the webhook has to prove before anything is stored#
The provider posts the message to Connect's inbound endpoint.
Result The request is verified against that provider's own scheme. An unverified post is refused and nothing is written — a webhook that anyone could forge would be a way to inject a message into a stranger's workspace.
The receiving number is resolved to one workspace.
Result Everything after this point runs inside that workspace, under the SQLAlchemy kernel and row-level security. A number that belongs to no configured workspace is a dead letter, not a message filed somewhere convenient.
The provider's message identifier is checked against what is already stored.
Result A retry updates the existing row instead of creating a second one. See Duplicate SMS for what that key is and what defeats it.
The sending number is resolved to a Person.
Result An existing identity attaches the message to that person's history; an unknown number creates a new contact rather than guessing at a match. Attaching an SMS to a person covers the near-misses.
The body is scanned for an opt-out word before it is treated as ordinary correspondence.
Result STOP and its siblings write a suppression and short-circuit the rest of the flow. A stop request answered by a sales reply is the worst failure this channel has.
What is actually stored#
| Record | What it holds | Who reads it |
|---|---|---|
| Provider-shaped row | The payload as the provider sent it | The adapter only — never the engine |
| Conversation | The thread this number's messages belong to | Conversations, the agent loop, the timeline |
| Message | Direction, text, timestamps, the provider's identifier | Everything downstream |
| Contact and identity | The number as one identity on a Person | Relationships, Customer 360, memory |
| Memory (only if a directive was set) | A tagged row such as block:sms | directives(), on every future decision |
Nothing about that list is SMS-specific, and that is the point. The channel contributes a transport and an opt-out convention; the record it produces is the same record every other channel produces.
What receiving alone does not give you#
The practical consequence is that SMS is an intake channel here rather than a dialogue channel. A workspace that receives an enquiry by text and replies by email or by phone is working with the grain of what exists. A workflow that assumes a text reply will go out is working against it, and will stall at the send. Sending SMS: what is available today is the page that spells that out, including what a provider that does carry SMS changes and what it does not.
In India there is a second condition even with such a provider: commercial SMS requires DLT registration — entity, header and template registration with the telecom operators. That is a process with the operators, not a setting, and it is described in DLT registration in India.
When receiving itself goes wrong#
- Nothing arrives at all
- Usually the provider has no webhook configured for that number, or is posting to an address that no longer exists. Connect cannot detect a message it was never sent.
- A message arrives with no person attached
- The sending number withheld caller identity, or arrived in a format the identity resolver did not recognise as the same number it already knows.
- The message arrives twice
- The provider retried and the identifier differed between attempts. That is the one case the dedupe key cannot cover.
- The message arrives late
- The provider queued it. The timestamp Connect stores is the provider's, so a delayed message does not silently pretend to be recent.
Questions#
Can Connect reply to a text message?
Not on the live carrier, which carries no SMS. With a provider that does carry SMS, an outbound message runs through the same send boundary and the same autonomy rule as an email — and in India it additionally needs DLT registration. Until one of those is in place, the reply is not written and the readiness on the Phone screen says why rather than showing a thread that cannot send.
Does an inbound SMS trigger the agent loop?
Yes. It becomes a canonical message like any other, so it is triaged, grounded against Knowledge and memory, and can produce a follow-up, a case or an item in Needs You. What it cannot produce today is an SMS reply.
Is the message text stored?
The body is stored on the canonical message, because a conversation you cannot read is not a record. It is scoped to one workspace by the isolation kernel and by row-level security in the database, and it is visible to that workspace's members and to nobody else.