What you need
- Salesforce Lightning Experience on an edition that allows Apex (Enterprise, Unlimited, Performance or Developer).
- System Administrator access, or someone who has it.
- A Texto account and API key. Signup is free and includes 5 credits to test with.
- A sandbox or Developer org for testing first is a good idea.
Step 1
Get a Texto API key
Create a free Texto account, then generate an API key from your account settings. Keep it somewhere safe; you'll paste it into Salesforce in the next step.
Get a free API keyStep 2
Store the key safely in Salesforce
This keeps your key out of the code and lets Salesforce attach it to every request.
- In Setup, search for Named Credentials and open the External Credentials tab. Click New.
- Label it
Texto, nameTexto, authentication protocol Custom. Save. - Under Principals, click New. Name it
TextoKey, then add an authentication parameter calledApiKeyand paste your Texto API key as the value. - Under Custom Headers, add a header named
Authorizationwith the valueBearer {!$Credential.Texto.ApiKey}. - Back on the Named Credentials tab, click New. Label and name it
Texto_API, URLhttps://api.texto.com.au, and choose the Texto external credential. Leave Generate Authorization Header unticked. Save. - Open a permission set your users have (or create one called Texto SMS), go to External Credential Principal Access and add
Texto - TextoKey. Assign the permission set to everyone who will send SMS.
Step 3
Add the Apex class
In the Developer Console (or VS Code with Salesforce extensions), create an Apex class called TextoSmsService and paste in the code below. It looks up the record's mobile, turns Australian numbers like 0412 345 678 into +61412345678, and sends the message to Texto.
public with sharing class TextoSmsService {
@AuraEnabled
public static String sendSms(Id recordId, String message, String sender) {
String to = getMobile(recordId);
if (String.isBlank(to)) {
throw new AuraHandledException('This record has no mobile number.');
}
if (String.isBlank(message)) {
throw new AuraHandledException('Type a message first.');
}
Map<String, Object> payload = new Map<String, Object>{
'to' => normalise(to),
'message' => message
};
// Leave sender out when blank so Texto uses your account default sender
if (String.isNotBlank(sender)) {
payload.put('sender', normalise(sender));
}
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Texto_API/send');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() != 200) {
throw new AuraHandledException('Texto error ' + res.getStatusCode() + ': ' + res.getBody());
}
Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) body.get('message_id');
}
private static String getMobile(Id recordId) {
if (recordId.getSObjectType() == Lead.SObjectType) {
Lead l = [SELECT MobilePhone, Phone FROM Lead WHERE Id = :recordId WITH SECURITY_ENFORCED LIMIT 1];
return String.isNotBlank(l.MobilePhone) ? l.MobilePhone : l.Phone;
}
Contact c = [SELECT MobilePhone, Phone FROM Contact WHERE Id = :recordId WITH SECURITY_ENFORCED LIMIT 1];
return String.isNotBlank(c.MobilePhone) ? c.MobilePhone : c.Phone;
}
// Turns 0412 345 678 into +61412345678. Leaves Sender IDs like "MyBiz" alone.
@TestVisible
private static String normalise(String value) {
String v = value.trim();
if (!Pattern.matches('^[0-9+()\\s-]+$', v)) return v;
v = v.replaceAll('[^0-9+]', '');
if (v.startsWith('04')) v = '+61' + v.substring(1);
else if (v.startsWith('61')) v = '+' + v;
return v;
}
}To deploy to production, Salesforce needs test coverage. Add this test class too:
@IsTest
private class TextoSmsServiceTest {
private class OkMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"message_id":"abc-123","credits_used":1,"credits_remaining":99}');
return res;
}
}
@IsTest
static void sendsToContactMobile() {
Contact c = new Contact(LastName = 'Test', MobilePhone = '0412 345 678');
insert c;
Test.setMock(HttpCalloutMock.class, new OkMock());
Test.startTest();
String id = TextoSmsService.sendSms(c.Id, 'Hello from Salesforce', '');
Test.stopTest();
System.assertEquals('abc-123', id);
}
@IsTest
static void normalisesNumbersButNotSenderIds() {
System.assertEquals('+61412345678', TextoSmsService.normalise('0412 345 678'));
System.assertEquals('MyBiz', TextoSmsService.normalise('MyBiz'));
}
}Step 4
Choose who the message comes from
The Send SMS box lets the user pick a sender. Here's what each option does:
Account default
No sender is sent to Texto, so the message goes out from your Texto account's default sender. Good if everyone should send from the same business name or number.
Sender ID or Texto number
Type a registered business Sender ID (like your business name) or a dedicated Texto number. It must already be approved on your Texto account. See how Sender ID registration works.
My mobile
The message goes out from the salesperson's own mobile, so replies come straight back to their phone. Salesforce supplies the number: the button reads the Mobile field on the logged-in user's Salesforce profile and sends it as the sender. Nobody has to type it.
Setting up "My mobile" for each rep
Each rep needs their own Texto login so they can verify their own mobile. Adding team members is free and there's no limit.
- Invite each rep as a team member in Texto. The account owner or an admin logs in to Texto, opens the team or users settings and invites a team member. Enter the rep's work email and give them permission to send. You can invite your whole sales team.
- Each rep accepts the invite and logs in. They open the invite email, set a password and log in to Texto.
- Each rep verifies their own mobile. In their own Texto login, they open the sender settings, add their mobile and enter the code Texto texts to that phone. Once verified, the number can be used as a sender on your account, including from Salesforce.
- Add the mobile to their Salesforce user. An admin goes to Setup > Users, edits the user and fills in Mobile. Or each rep opens their avatar > Settings > My Personal Information > Personal Information and adds it there.
- Use the same number in both places. If the number in Salesforce isn't verified in Texto, the send is rejected and the rep sees the error in Salesforce.
The Send SMS button still uses the one shared API key from Step 2. Reps don't need their own API key; their Texto login is only for verifying their number.
Managing lots of users or sub-accounts? Team members can also be invited through the API (POST /account/:id/users with send permission). See the SMS API reference.
Step 5
Add the Lightning component
Create a Lightning Web Component called textoSendSms with these three files. It shows the contact's number, the sender choice, a message box with a live character and SMS count, and a Send button.
<template>
<lightning-quick-action-panel header="Send SMS">
<p class="slds-m-bottom_small">To: <strong>{toNumber}</strong></p>
<lightning-radio-group
label="Send from"
options={senderOptions}
value={senderMode}
onchange={handleSenderMode}>
</lightning-radio-group>
<template if:true={isCustom}>
<lightning-input
label="Sender ID or Texto number"
value={customSender}
onchange={handleCustomSender}>
</lightning-input>
</template>
<p class="slds-text-color_weak slds-m-vertical_small">{senderHelp}</p>
<lightning-textarea
label="Message"
value={message}
onchange={handleMessage}
max-length="918">
</lightning-textarea>
<p class="slds-text-color_weak">{message.length} characters, {segments} SMS</p>
<div slot="footer">
<lightning-button label="Cancel" onclick={close}></lightning-button>
<lightning-button variant="brand" label="Send" class="slds-m-left_x-small"
disabled={sending} onclick={send}></lightning-button>
</div>
</lightning-quick-action-panel>
</template>import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import { CloseActionScreenEvent } from 'lightning/actions';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import USER_ID from '@salesforce/user/Id';
import USER_MOBILE from '@salesforce/schema/User.MobilePhone';
import sendSms from '@salesforce/apex/TextoSmsService.sendSms';
export default class TextoSendSms extends LightningElement {
@api recordId;
@api objectApiName;
message = '';
senderMode = 'default';
customSender = '';
sending = false;
// The record's own mobile (Contact or Lead)
@wire(getRecord, { recordId: '$recordId', layoutTypes: ['Compact'], optionalFields: ['Contact.MobilePhone', 'Lead.MobilePhone'] })
record;
// The logged-in Salesforce user's Mobile field (Settings > My Personal Information)
@wire(getRecord, { recordId: USER_ID, fields: [USER_MOBILE] })
user;
get toNumber() {
const f = this.record?.data?.fields?.MobilePhone;
return f?.value || 'No mobile on this record';
}
get myMobile() {
return getFieldValue(this.user?.data, USER_MOBILE);
}
get senderOptions() {
return [
{ label: 'Account default', value: 'default' },
{ label: 'My mobile' + (this.myMobile ? ' (' + this.myMobile + ')' : ''), value: 'mine' },
{ label: 'Sender ID or Texto number', value: 'custom' }
];
}
get isCustom() { return this.senderMode === 'custom'; }
get sender() {
if (this.senderMode === 'mine') return this.myMobile || '';
if (this.senderMode === 'custom') return this.customSender;
return '';
}
get senderHelp() {
if (this.senderMode === 'mine') {
return this.myMobile
? 'Will send from your mobile ' + this.myMobile + '. It must be verified in Texto.'
: 'Add your mobile in Salesforce (Settings > My Personal Information) to use this.';
}
if (this.senderMode === 'custom') return 'Must be a Sender ID or number already approved on your Texto account.';
return 'Will send from your Texto account default sender.';
}
get segments() {
const len = this.message.length;
return len <= 160 ? 1 : Math.ceil(len / 153);
}
handleSenderMode(e) { this.senderMode = e.detail.value; }
handleCustomSender(e) { this.customSender = e.detail.value; }
handleMessage(e) { this.message = e.detail.value; }
close() { this.dispatchEvent(new CloseActionScreenEvent()); }
async send() {
if (this.senderMode === 'mine' && !this.myMobile) {
this.toast('No mobile on your user', this.senderHelp, 'warning');
return;
}
this.sending = true;
try {
await sendSms({ recordId: this.recordId, message: this.message, sender: this.sender });
this.toast('SMS sent', 'Your message is on its way.', 'success');
this.close();
} catch (err) {
this.toast('SMS not sent', err?.body?.message || 'Something went wrong.', 'error');
} finally {
this.sending = false;
}
}
toast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>ScreenAction</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>Deploy the component and both Apex classes to your org.
Step 6
Create the Send SMS button
- In Setup, go to Object Manager > Contact > Buttons, Links, and Actions and click New Action.
- Action Type Lightning Web Component, component
c:textoSendSms, label Send SMS. Save. - Open Page Layouts (or the Lightning Record Page in App Builder), and drag Send SMS into the Salesforce Mobile and Lightning Experience Actions area. Save.
- Repeat for Lead if you want the button there too.
Step 7
Send a test message
Open a contact with your own mobile in the Mobile field, click Send SMS, type a message and send. You'll see a "SMS sent" message in Salesforce and the text on your phone within seconds. Try each sender option to check they all work.
Optional: log each SMS on the record
To keep a history, add this inside sendSms after a successful response. It creates a completed Task on the contact or lead with the message text.
Task t = new Task(Subject = 'SMS sent via Texto', Description = message, Status = 'Completed', ActivityDate = Date.today()); t.WhoId = recordId; // works for both Contacts and Leads insert t;
Troubleshooting
- Texto error 401: the API key is wrong or missing. Check the principal in your External Credential and the Authorization header.
- Texto error 402: your Texto account is out of credits. Top up in the dashboard.
- Texto error 400: the mobile number isn't valid, or the sender isn't approved or verified on your Texto account.
- "This record has no mobile number": fill in Mobile (or Phone) on the contact.
- Unauthorized endpoint or no access to credential: the user is missing the permission set from Step 2.
FAQs
Can I add a Send SMS button to Salesforce contacts?
Yes. With an Apex class and a small Lightning component you can add a Send SMS quick action to Contacts and Leads. The user types a message in Salesforce and it is sent through the Texto SMS API.
Can each salesperson send from their own mobile number?
Yes. Invite each rep as a team member in Texto, then they log in and verify their mobile once and add it to the Mobile field on their Salesforce user profile. When they pick My mobile, the button reads that number and sends it as the sender, so replies go straight to their phone.
Do my sales reps need their own Texto login?
Only if they want to send from their own mobile. They need a login to verify their number. Team members are free and unlimited, and reps don't need their own API key.
What happens if I leave the sender empty?
Texto sends from your account default sender. You can see and change the default on the Sender ID page in your Texto dashboard.
How much does it cost?
3¢ per SMS to Australian numbers and 9¢ to New Zealand. There are no monthly fees, and new accounts get 5 free credits to test with.
Can I use a business name as the sender?
Yes, once your Sender ID is registered. See the Sender ID page for how registration works.
How do I add an unsubscribe link for marketing messages?
Put {{OptOutLink}} in the message. Each recipient gets their own one-tap opt-out link, and the number is added to your opt-out list once they confirm.
Does this need an AppExchange package?
No. It uses standard Salesforce features: a Named Credential, an Apex class and a Lightning Web Component. It works in Lightning Experience on editions that allow Apex.
Related