Skip to content

External SMS Capture

When you use a BYO Twilio account, SMS may be sent through other platforms (Twilio Console, another CRM, custom scripts). Loquent captures these external messages automatically through the same event sink used for delivery status tracking.

When a status event arrives for a message Loquent didn’t send, it creates the record instead of ignoring it:

Twilio status event (sent/delivered/failed)
→ Look up message by external_id
→ Not found? → This is an external message
→ Determine direction from phone ownership
→ Resolve or create contact
→ Create message record (source: "external", body: null)
→ Publish EVT_MESSAGE_NEW event
→ Background: fetch body from Twilio API
→ Background: update contact last_contacted_at

Previously, status events for unknown messages were silently dropped. Now they trigger message creation with the source field set to "external".

The source field on message tracks where a message originated:

ValueMeaning
"loquent"Sent through Loquent’s UI or text agent
"external"Sent via another platform (Twilio Console, third-party CRM)
"import"Imported during message history sync

The field is nullable — messages created before this feature have source: null.

External messages arrive without body text because Twilio status events don’t include it. Loquent fetches the body asynchronously:

  1. After creating the message, a background task calls the Twilio Messages API:
    GET /Accounts/{account_sid}/Messages/{message_sid}.json
  2. Updates the message record with the body text
  3. Publishes an EVT_MESSAGE_STATUS event so the UI populates the message content
src/mods/twilio/utils/fetch_twilio_message_util.rs
pub async fn fetch_twilio_message_body(
account_sid: &str,
auth_token: &str,
message_sid: &str,
) -> Result<Option<String>, AppError>

Uses HTTP Basic Auth with the org’s Twilio credentials. Returns None if the message body is empty.

Multiple Twilio events for the same message SID can arrive concurrently (e.g., sent and delivered in rapid succession). Loquent handles this with a unique constraint on (organization_id, external_id):

match create_message(input).await {
Ok(msg) => msg,
Err(AppError::Database(ref e)) if is_unique_violation(e) => {
// Another event already created it — update status instead
update_message_status(&message_sid, status).await?
}
Err(e) => return Err(e),
}

This ensures exactly one message record per Twilio SID, regardless of event ordering.

The messaging sidebar subscribes to EVT_MESSAGE_NEW events and automatically re-fetches the contact list when external messages arrive. Contacts with new messages move to the top of the list.

src/mods/messaging/components/messaging_contact_list_component.rs
let ctx = use_context::<RealtimeContext>();
let mut refresh_key = use_signal(|| 0u32);
use_effect(move || {
let events = (ctx.events)();
// Check for new message events since last seen
if has_new_message_events {
refresh_key += 1; // Triggers contact list re-fetch
}
});

The contact list resource depends on refresh_key, so incrementing it triggers a fresh API call and re-sort by last_contacted_at.

EventConstantWhen Published
messaging.message.newEVT_MESSAGE_NEWExternal message created (body may be null initially)
messaging.message.statusEVT_MESSAGE_STATUSBody enriched or status updated

The conversation feed hook (use_realtime_messages) handles both: it appends new messages and updates existing ones as body text and status arrive.

  1. User sends SMS via Twilio Console (not Loquent)
  2. Twilio fires a message.sent event to /twilio/events
  3. handle_status_update() finds no matching message in the database
  4. Determines direction: from is an org number → outbound
  5. Resolves or creates a contact for the to number
  6. Creates a message with source: "external", body: null, status: sent
  7. Publishes EVT_MESSAGE_NEW — message appears in the feed (no body yet)
  8. Background task fetches body from Twilio API, updates the record
  9. Publishes EVT_MESSAGE_STATUS — body text appears in the UI
  10. Contact list re-fetches, contact moves to the top
  11. Later message.delivered event updates status via the normal path
FilePurpose
src/mods/twilio/api/twilio_events_api.rsStatus event handling with create-on-miss logic
src/mods/twilio/utils/fetch_twilio_message_util.rsTwilio Messages API call for body enrichment
src/mods/messaging/services/update_message_status_service.rsReturns Option<Message> (None = not found)
src/mods/messaging/components/messaging_contact_list_component.rsSSE-driven contact list refresh
migration/src/m20260310_000000_message_add_source.rsAdds source column to message table