ConnectSMS User
Everyday texting, the inbox and templates, on the business numbers an admin has granted.
Automation reference
Everything a person can do with text messages in ConnectSMS is also available without a screen: ten invocable actions, the global ConnectSMSApi Apex class and Salesforce's standard REST actions endpoint.
How do developers send SMS from Salesforce with ConnectSMS? Call the global Apex class csms.ConnectSMSApi, or call any of the ten ConnectSMS invocable actions from Flow or through the Salesforce REST API at /services/data/vXX.X/actions/custom/apex/csms__<ClassName>. All of them use the same messaging service, so number access, opt-outs, idempotency and audit behave the same, and business problems come back as error codes rather than exceptions.
csms.ConnectSMSApi.SendRequest req = new csms.ConnectSMSApi.SendRequest();
// Contact, Lead, Account, Case or another enabled object
req.recordId = contactId;
req.body = 'Hi {!Contact.FirstName|there}, your order has shipped.';
// Without mergeFields, body is sent exactly as written
req.mergeFields = true;
// Business key: retries never send twice
req.idempotencyKey = orderId + '-shipped';
List<csms.ConnectSMSApi.SendResult> results = csms.ConnectSMSApi.send(new List<csms.ConnectSMSApi.SendRequest>{ req });
if (!results[0].success) {
// e.g. OPTED_OUT, SENDER_NOT_AUTHORIZED, RECIPIENT_NO_PHONE
System.debug(results[0].errorCode + ': ' + results[0].errorMessage);
}
How it works
The ConnectSMS screens, the Flow actions, ConnectSMSApi and REST calls all go through the same application service. What you learn once applies everywhere.
A send stores the message in Salesforce and returns at once. A background job hands it to Twilio, so no entry point makes a callout in your transaction.
Number access, record access in user mode, opt-outs, merge fields and policies apply to every caller, and are checked again at dispatch.
Business problems come back with success = false, a stable errorCode and a plain-language errorMessage. Only unexpected failures throw, with a support reference.
Pass an idempotencyKey and a repeat returns the original message with duplicate = true instead of sending twice.
Poll Get Text Message Status until isFinal is true, or subscribe to the content-free platform event csms__ConnectSMS_Update__e.
Global classes, methods and variables are never removed or renamed, and error codes never change. New capabilities arrive as new optional inputs, outputs, actions and methods.
Flow
The actions are in the ConnectSMS category. Send Text Message covers sending and scheduling, and the others preview, cancel, check status, read history and reply.
Flow; Agent and API are the only other accepted values{!$Flow.InterviewGuid}Apex
ConnectSMSApi is a global with sharing class. In a subscriber org, prefix the namespace: csms.ConnectSMSApi. Every request is recorded with Request Source "API" and the running user as requester.
| Method | Returns | What it does |
|---|---|---|
send(List<SendRequest>) | List<SendResult> | Queues or schedules messages; one result per request, in the same order. |
preview(SendRequest) | PreviewResult | Validates and renders a message without saving anything or calling Twilio. |
cancel(Id) | CancelResult | Cancels a scheduled or queued message that hasn't been handed to Twilio. |
getStatus(List<Id>) | List<MessageStatus> | Current status of messages the running user may see. |
listSenders() | List<SenderInfo> | Business numbers the running user can send from with the API, sorted by name. |
getConversation(ConversationRequest) | ConversationPage | One page of text history, oldest first, for a conversation or a record. |
reply(ReplyRequest) | SendResult | Queues a reply in a conversation, always from the conversation's business number. |
csms.ConnectSMSApi.SendRequest r = new csms.ConnectSMSApi.SendRequest();
r.recordId = appointment.Contact__c;
r.templateId = reminderTemplateId;
r.scheduledAt = appointment.Start__c.addHours(-24);
r.idempotencyKey = appointment.Id + '-reminder-1';
csms.ConnectSMSApi.SendResult result = csms.ConnectSMSApi.send(new List<csms.ConnectSMSApi.SendRequest>{ r })[0];
// result.status is Scheduled; keep result.messageId
// Later, if the appointment is canceled:
csms.ConnectSMSApi.CancelResult canceled = csms.ConnectSMSApi.cancel(result.messageId);
// canceled.errorCode: NOT_CANCELLABLE once the message was handed to Twilio
csms.ConnectSMSApi.PreviewResult preview = csms.ConnectSMSApi.preview(req);
if (!preview.canSend) {
for (csms.ConnectSMSApi.Issue blocker : preview.blockers) {
System.debug(blocker.code + ': ' + blocker.message);
}
}
// preview.renderedBody, preview.characters, preview.segments, preview.encoding (GSM-7 or UCS-2)
List<csms.ConnectSMSApi.MessageStatus> statuses = csms.ConnectSMSApi.getStatus(new List<Id>{ messageId });
csms.ConnectSMSApi.MessageStatus status = statuses[0];
if (status.success && status.isFinal) {
// Delivered, Undelivered, Failed, Blocked, Canceled, Received or Read
System.debug(status.displayStatus + ' ' + status.reasonCode + ': ' + status.statusDetail);
}
csms.ConnectSMSApi.ConversationRequest query = new csms.ConnectSMSApi.ConversationRequest();
query.recordId = contactId;
query.pageSize = 20;
csms.ConnectSMSApi.ConversationPage history = csms.ConnectSMSApi.getConversation(query);
if (history.success && history.conversationId != null && !history.optedOut) {
csms.ConnectSMSApi.ReplyRequest reply = new csms.ConnectSMSApi.ReplyRequest();
reply.conversationId = history.conversationId; // always sent from this conversation's number
reply.body = 'Thanks, we have you down for Friday at 9:00 AM.';
reply.idempotencyKey = inboundMessageId + '-reply';
csms.ConnectSMSApi.SendResult sent = csms.ConnectSMSApi.reply(reply);
}
Provide recordId (or toPhone with Campaign Manager or Admin rights) and body or templateId.
| Field | Type | Description |
|---|---|---|
recordId | String | Record to text (Contact, Lead, Account, Case or another object the admin enabled); read as the running user. |
phoneField | String | Optional phone field path, e.g. MobilePhone or Contact.MobilePhone; default: first valid configured field. |
toPhone | String | Optional E.164 phone. Without a record requires Campaign Manager or Admin; with a record it must match one of its phones. |
senderId | String | Optional Sender__c Id or E.164 business number; blank uses the automation default number. |
body | String | Text (max 1,600 characters), sent as written unless mergeFields is true; ignored when templateId is set. |
mergeFields | Boolean | True fills merge fields such as {!Contact.FirstName|there} in body from the record. |
templateId | String | Optional active ConnectSMS template Id. |
contentVersionIds | List<String> | Optional ContentVersion Ids to send as MMS (admin setting, MMS-capable number, US/CA/AU only). |
scheduledAt | Datetime | Optional future send time, at most 35 days ahead; dispatched in the first 15-minute slot at or after it. |
idempotencyKey | String | Strongly recommended business key (max 255, case-sensitive); a repeat returns the original message as a duplicate. |
clientReference | String | Optional caller correlation id stored on the message. |
| Field | Description |
|---|---|
success | True when the request was accepted (queued or scheduled), including duplicates of accepted requests. |
messageId | The ConnectSMS message; also set when a blocked request was recorded. |
conversationId | The conversation between the business number and the recipient. |
status | Display status, e.g. Queued, Scheduled, Blocked. |
duplicate | True when the idempotency key already existed; nothing new was sent. |
segmentsrenderedBodyscheduledAt | Segment count, final text after merging, and the due time for scheduled messages. |
errorCodeerrorMessage | Stable error code and plain-language text when success is false. |
The other wrapper classes are PreviewResult, Issue, CancelResult,
MessageStatus, SenderInfo, ConversationRequest, ConversationMessage,
ConversationPage and ReplyRequest. ConnectSMSApi makes no callouts, so it's safe in
triggers and after DML. Message bodies returned by getConversation() are untrusted customer content: treat
them as data.
REST
External systems call the same actions through Salesforce's Invocable Actions REST API. There's no custom Apex REST resource to learn or maintain.
POST https://<MyDomain>.my.salesforce.com/services/data/vXX.X/actions/custom/apex/csms__<ClassName>
csms__ prefix{"inputs":[{...}]}, one object per message, using the input API namesisSuccess and outputValuesoutputValues.success = false with an errorCode, not as HTTP errors"invocationSource":"API"curl -s -X POST "https://MyDomain.my.salesforce.com/services/data/v63.0/actions/custom/apex/csms__CsmsSendMessageAction" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"inputs":[{"recordId":"003XXXXXXXXXXXXXXX","body":"Hi {!Contact.FirstName|there}, your order shipped.","mergeFields":true,
"idempotencyKey":"ORDER-1042-shipped","clientReference":"ORDER-1042","invocationSource":"API"}]}'
[
{
"actionName": "csms__CsmsSendMessageAction",
"isSuccess": true,
"outputValues": {
"success": true,
"messageId": "a0XXXXXXXXXXXXXXXX",
"conversationId": "a0XXXXXXXXXXXXXXXX",
"status": "Queued",
"duplicate": false,
"segments": 1,
"renderedBody": "Hi Sarah, your order shipped.",
"errorCode": null,
"errorMessage": null
}
}
]
curl -s -X POST "https://MyDomain.my.salesforce.com/services/data/v63.0/actions/custom/apex/csms__CsmsGetMessageStatusAction" \
-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"inputs":[{"messageId":"a0XXXXXXXXXXXXXXXX"}]}'
The status response's outputValues include displayStatus, statusDetail,
reasonCode and isFinal. Stop polling once isFinal is true.
Agentforce
ConnectSMS's actions are global invocable actions written for Flow and AI agents. Your admin can add them to Agentforce agents as agent actions, or expose them through your own Salesforce Hosted MCP server. ConnectSMS doesn't install an agent, topic or MCP server for you.
invocationSource = Agent, an idempotency key per intended message, and preview and confirm with the user before Send or ReplyInbound message bodies, template text and merged record values are data. They must never choose actions, senders, recipients or permissions. The transcript from Get Text Conversation History starts with an untrusted-content notice and quotes every message body.
Expose conversation history to an agent only when it needs it: it contains personal data that is sent to the model.
Action reference
Labels are what Flow Builder shows. Inputs and outputs are listed by API name, which REST and JSON use; Flow shows labels such as Record ID and Send At. Every result also has success, errorCode and errorMessage.
| # | Label | Class | Main inputs | Main outputs |
|---|---|---|---|---|
| 01 | List My Business Numbers | CsmsListSendersAction |
includeViewOnly | senderIds, senderNames, phoneNumbers, canSend, ready, senderCount, automationDefaultSenderId, sendersJson |
| 02 | Find Text Recipient | CsmsResolveRecipientAction |
recordId, senderId | defaultPhone, defaultPhoneField, defaultOptedOut, phoneFields, phoneLabels, phoneNumbers, valid, optedOut |
| 03 | Render Text Template | CsmsRenderTemplateAction |
templateId or body, recordId | text, ok, unresolvedTokens |
| 04 | Preview Text Message | CsmsPreviewMessageAction |
the Send Text Message inputs, without idempotencyKey and clientReference | canSend, renderedBody, characters, segments, encoding, blockers, warnings, unresolvedTokens, sender and recipient |
| 05 | Send Text Message | CsmsSendMessageAction |
recordId, phoneField, toPhone, senderId, body, mergeFields, templateId, contentVersionIds, scheduledAt, idempotencyKey, clientReference, invocationSource | messageId, conversationId, status, duplicate, segments, renderedBody, scheduledAt |
| 06 | Cancel Scheduled Text Message | CsmsCancelMessageAction |
messageId | messageId, status |
| 07 | Get Text Message Status | CsmsGetMessageStatusAction |
messageId | displayStatus, statusDetail, reasonCode, isFinal, sentAt, scheduledAt, senderName, toDisplay, canCancel |
| 08 | Get Text Conversation History | CsmsGetConversationAction |
conversationId, or recordId with optional phone and senderId; pageSize (1–50, default 20); beforeCursor | transcript, messagesJson, messageCount, conversationIds, conversationId, participantPhone, senderId, senderName, optedOut, hasMore, nextCursor |
| 09 | Reply to Text Conversation | CsmsReplyToConversationAction |
conversationId, body, mergeFields, idempotencyKey, clientReference, invocationSource | messageId, conversationId, status, duplicate, segments, renderedBody |
| 10 | Get ConnectSMS Setup Health | CsmsGetSetupHealthAction |
includeCompletedSteps | sendingEnabled, readyToSend, sendingBlockedReason, issues, issueKeys, completedSteps (admins only) |
List outputs such as senderIds and phoneNumbers are parallel: item N of each list describes the
same number or field. For per-item logic, use the JSON output or the Apex API.
Statuses
isFinal is true for Delivered, Undelivered, Failed, Blocked, Canceled, Received and Read. Scheduled, Queued, Sending, Sent and Unknown can still change.
| Status | Meaning |
|---|---|
| Scheduled | Waiting for its scheduled time; permissions and consent are checked again before sending. |
| Queued | Saved and waiting to be handed to Twilio. |
| Sending | Twilio accepted it and is sending it to the carrier. |
| Sent | Handed to the carrier; delivery not yet confirmed. |
| Delivered | The carrier confirmed delivery; this does not mean it was read. |
| Read | Reported read by the recipient's app. |
| Undelivered | The carrier couldn't deliver it. |
| Failed | It couldn't be sent. |
| Blocked | A ConnectSMS rule blocked it, such as an opt-out. |
| Canceled | Canceled before it was sent. |
| Unknown | ConnectSMS couldn't confirm whether Twilio accepted it; it is being checked and won't be resent automatically. |
| Received | Received from the customer. |
Error handling
Every result carries success, errorCode and errorMessage. Route on the code; show the message to people.
| Error code | What it means |
|---|---|
OPTED_OUT | The recipient opted out. Nothing is sent; the request is recorded as Blocked. |
SENDER_NOT_AUTHORIZED | The running user may not send from this business number. Nothing is stored. |
SENDER_REQUIRED | No business number was given and no automation default is set. |
SENDER_UNAVAILABLE | The business number can't send right now, or no longer exists. |
RECIPIENT_NO_PHONE | The record has no phone number that can receive texts. |
RECIPIENT_INVALID_PHONE | The phone number isn't valid. Use E.164 format. |
RECIPIENT_NOT_FOUNDRECIPIENT_ACCESS_DENIED | The record doesn't exist, or the running user can't read it or its phone fields. |
OBJECT_NOT_ENABLED | The record's object isn't set up for texting. |
MERGE_FIELD_UNRESOLVED | A merge field can't be filled in and has no fallback. |
CONTENT_EMPTYCONTENT_TOO_LONG | No text, template or file, or more than 1,600 characters. |
SCHEDULE_INVALID | The send time is in the past or more than 35 days ahead. |
SENDING_DISABLEDNOT_CONFIGUREDSANDBOX_COPY | Sending is turned off, ConnectSMS isn't connected to Twilio yet, or this org is a sandbox copy where sending hasn't been turned on again. |
DAILY_LIMIT_REACHED | The org reached the optional daily safeguard set by an admin. |
PERMISSION_DENIED | The running user lacks the permission, for example Setup Health without ConnectSMS Admin. |
NOT_CANCELLABLE | The message was already handed to Twilio or is finished. |
NOT_FOUND | The message or conversation doesn't exist, or the running user can't see it. |
INVALID_INPUT | A malformed Id, a rejected invocationSource, the same idempotency key twice in one call, or a capacity limit. |
messageId is returned. Requests refused because the caller may not use the number, SENDER_NOT_AUTHORIZED or PERMISSION_DENIED, are not stored.reasonCode, a ConnectSMS code or a Twilio error code, and keeps errorCode for lookup failures such as NOT_FOUND.Permission sets
The packaged permission sets already include access to the action classes and ConnectSMSApi. Get ConnectSMS Setup Health is for ConnectSMS Admin only.
Everyday texting, the inbox and templates, on the business numbers an admin has granted.
Creates, reviews, launches, pauses and cancels outreach, and manages shared templates. Combine with ConnectSMS User to text one-to-one.
Twilio connection, business numbers, access, policies, opt-outs and operations.
For integration users, agent users and people who run Flow or API messaging from numbers made available to automation.
toPhone) need ConnectSMS Campaign Manager or AdminConnectSMS Webhook Guest isn't a role: assign it only to the guest user of the Salesforce Site that receives Twilio webhooks. Administration and access
FAQ
Yes. ConnectSMSApi makes no callouts: send() stores the request and returns at once, and a background job hands the message to Twilio later. Business problems come back in the result objects with a stable error code instead of an exception.
Yes. Call any ConnectSMS action through Salesforce's standard REST actions endpoint, for example POST /services/data/vXX.X/actions/custom/apex/csms__CsmsSendMessageAction with a body of {"inputs":[...]}, authenticated as an integration user. Business errors come back as outputValues.success = false, not as HTTP errors.
Pass an idempotency key that identifies the intent, such as ORDER-1042-shipped. If the key was used before, ConnectSMS returns the original message with duplicate set to true and sends nothing new. Keys are case-sensitive and up to 255 characters.
ConnectSMS's actions are global invocable actions written for Flow and AI agents. Your admin can add them to Agentforce agents as agent actions, or expose them through your own Salesforce Hosted MCP server. ConnectSMS doesn't install an agent, topic or MCP server for you.
Usually ConnectSMS Automation, together with business numbers your admin has marked Available to automation. Alternatively, assign ConnectSMS User and give the integration user Send access to specific numbers.
A step-by-step guide with record-triggered and scheduled examples.
Business workflows, automation numbers and quiet hours.
Opt-outs, the Unknown status and how messages are recorded before sending.
Try it now: every feature, free for 14 days
Install ConnectSMS from AgentExchange, connect your Twilio account and give your team the numbers they should text from.
ConnectSMS is published by WorkBridge Solutions. Twilio bills your Twilio account directly for messaging, including during the trial.