When I finished my DocuSign + Salesforce Flow project, I wanted to go further. DocuSign was powerful, but it was mostly declarative — an AppExchange package did the heavy lifting. I wanted to build something where I owned every layer of the integration.

So I built a Twilio SMS integration in Salesforce. No AppExchange package. No pre-built connector. Just Apex, Named Credentials, Custom Metadata, and Flow — wired together from scratch.

And yes, I used AI as a coding partner for the Apex parts. I'll explain exactly what that meant and why I think it's worth being open about.


What It Does

When an Opportunity is marked Closed Won in Salesforce, the automation:

Everything automated. Everything traceable. Nothing silent.


Why This Project Is Different From DocuSign

My previous project used the DocuSign Apps Launcher — an AppExchange package that handled authentication, templates, and Flow actions out of the box. Powerful, but declarative. This project has no package.

LayerDocuSign ProjectTwilio Project
AuthenticationPackage handled OAuthNamed Credentials — built manually
API callsPre-built Flow actionCustom Apex callout
ConfigurationPackage settings UICustom Metadata Type
Flow actionInstalled with packageBuilt as Invocable Apex Method
Code requiredZeroApex class + Test class

💡 The real value

The Twilio project forced me to understand what the DocuSign package was doing silently. That understanding is the real portfolio value here.


The Stack — All Free to Start

ToolPurposeCost
Salesforce Developer OrgFlow, Apex, Named CredentialsFree
Twilio Trial AccountSMS delivery via REST APIFree trial credit (~$15)
Custom MetadataSecure config storageFree — native Salesforce
Named CredentialsSecure API authenticationFree — native Salesforce

💳 Twilio trial note

Trial accounts come with free credit to purchase a sending number and make real API calls — no credit card required. Buy the number using trial credit, and release it once testing is done to preserve your remaining balance.


1

Configuration

Custom Metadata: Config Without Hardcoding

The first decision was where to store Twilio credentials and phone numbers. Three options exist:

Custom Metadata won. I created a Twilio_Config__mdt type with three fields:

FieldWhat it stores
Account_SID__cTwilio Account SID
Twilio_Phone_Number__cThe number SMS is sent from
Fallback_Phone_Number__cBackup number if owner has none

In Apex, retrieving this requires no SOQL — one line, no governor limit impact:

Twilio_Config__mdt config = Twilio_Config__mdt.getInstance('Default');
Custom Metadata — Twilio_Config__mdt type with Account SID, phone number and fallback fields

Custom Metadata — Twilio_Config__mdt setup

2

Authentication

Named Credentials: Keeping Auth Token Out of Code

The Twilio Auth Token is essentially a password — it should never appear in Apex code, Custom Fields, or debug logs. Named Credentials solve this.

External Credential setup

Named Credential setup

In Apex, Salesforce injects the authentication header automatically — the Auth Token never appears in code:

req.setEndpoint('callout:Twilio/2010-04-01/Accounts/' + accountSid + '/Messages.json'); // Auth Token never touches the code. Salesforce handles it.

🐛 Undocumented gotcha

Permission Set Mappings for External Credentials are no longer configured on the External Credential page. Go to Permission Set or Profile → External Credential Principal Access and enable it from there. This is completely undocumented and cost me real time.

Named Credential linked to External Credential with Principals configured

Named Credential → External Credential → Principals

Named Credential detail showing Twilio URL, External Credential link and callout options

Named Credential detail — URL, authentication and callout settings

Permission Set showing External Credential Principal Access setting

Permission Set — External Credential Principal Access

3

Development

The Apex Class: Where AI Came In

I used AI as a coding partner. In a real project, speed and accuracy matter as much as authorship. I reviewed every line, understood every decision, and debugged every error myself. Here's what each part does and why it matters:

@InvocableMethod — makes it callable from Flow

@InvocableMethod(label='Send Twilio SMS') public static void sendSMS(List<SMSRequest> requests) { ... } // Without this annotation, Flow cannot see the class at all.

SMSRequest inner class — the input contract

Defines what data Flow passes into Apex. Each @InvocableVariable becomes a mappable field in the Flow action — the contract between Flow and Apex.

@future(callout=true) — the most important thing I learned

@future(callout=true) private static void sendSMSAsync(String toNumber, String oppName) { ... }

⚡ Non-negotiable

Flow-triggered Apex cannot make HTTP callouts in the same transaction. The @future annotation runs the callout asynchronously. This is the standard Salesforce pattern — skip it and the class throws an error every time.

Phone number fallback logic

String toNumber = (req.ownerPhone != null && req.ownerPhone != '') ? req.ownerPhone : fallbackNumber; // Ternary operator: one-line if/else. Owner phone or Custom Metadata fallback.
4

Testing

Apex Test Class: 100% Coverage

Every Apex class needs a test class with at least 75% coverage to deploy. I hit 100%. The key pattern for testing HTTP callouts is HttpCalloutMock — Salesforce blocks real HTTP calls in test context:

private class TwilioMockResponse implements HttpCalloutMock { public HTTPResponse respond(HTTPRequest req) { HttpResponse res = new HttpResponse(); res.setStatusCode(201); res.setBody('{"sid": "SM123", "status": "queued"}'); return res; } }

Four test methods covered:

5

Automation

The Flow: Orchestrating Everything

Record-Triggered Flow on Opportunity, firing when Stage = Closed Won. The phone number logic was an interesting design decision — a single Decision element with three outcomes, both paths feeding one variable:

OutcomeConditionAction
Has PhoneOwner.Phone is not nullAssign to varRecipientPhone
Has MobileOwner.MobilePhone is not nullAssign to varRecipientPhone
DefaultNeither field has a valueCreate High Priority Task

One variable, one Apex call — regardless of which path was taken. Cleaner than two nested Decision elements.

Opportunity → Closed Won

    ↓

Check Owner phone (Phone → Mobile fallback)

  ↓ Neither found           ↓ Number found

Create Task         Call Apex → Twilio SMS

(no number)            ↓ Success       ↓ Fault

             SMS_Sent__c = ✅    Task (error msg)

             Chatter post on Opportunity

Salesforce Flow canvas showing the full Send SMS on Closed Won automation

Flow canvas — Send SMS on Closed Won


The Gotchas — What No Tutorial Will Tell You

Gotcha 01
A2P 10DLC blocks US trial numbers
Since 2023, US carriers require all application-to-person SMS traffic to be registered. Twilio trial accounts cannot complete this registration. Workaround: buy a Canadian Twilio number — no A2P registration required. In production, registration happens entirely on the Twilio side — the Salesforce code doesn't change at all.
Gotcha 02
Geo Permissions are off by default
Even with a non-US sending number, Twilio blocks delivery to regions not explicitly enabled. Go to Messaging → Geographic Permissions in the Twilio console and enable the countries you need. Completely invisible until you hit the error — applies to all numbers.
Gotcha 03
Named Credential setup has changed
Permission Set Mappings used to live on the External Credential page. They don't anymore. Configure from Permission Set or Profile → External Credential Principal Access. The old UI is gone and the documentation hasn't caught up.
Gotcha 04
@future is mandatory for Flow callouts
Flow-triggered Apex runs synchronously. HTTP callouts from synchronous Apex triggered by Flow throw an error. Always wrap your callout in a @future method. Always.
Twilio trial account dashboard showing free credit balance

Twilio trial account with free credit

Twilio console showing how to release a phone number to preserve trial credit

Release phone number after testing to preserve credit

Twilio note showing receiving number must be verified on trial accounts

Receiving number must be verified to receive SMS on trial accounts

Twilio Messaging Geographic Permissions showing destination countries enabled

Geo Permissions — enable destination countries before testing

Salesforce debug logs showing successful HTTP callout to Twilio API

Salesforce debug logs — successful callout


End-to-End Results

Salesforce Opportunity record showing SMS Sent checkbox updated to True

Opportunity — SMS Sent checkbox updated to True after successful send

Mobile phone showing SMS received from the Twilio integration

SMS received on mobile

Salesforce Chatter post on Opportunity confirming SMS was sent

Chatter post on Opportunity confirming SMS sent


On Using AI as a Coding Partner

I used AI to help write the Apex class. I did not use it to think through the architecture, design the edge cases, debug the errors, or make the decisions about how pieces connected.

If you're a Salesforce Admin thinking about crossing into development — AI makes that crossing more accessible than ever. But understanding what you build still matters.

Understanding the code matters more than writing it. In a real team, you'll review others' Apex constantly. Being able to read, explain, and debug Apex is the skill. It always will be.

Try It Yourself

Everything here uses free tools. The full build — Custom Metadata, Named Credentials, Apex class, Test class, and Flow — takes about a day if you follow this guide and know where the gotchas are. Now you do.

Built on Salesforce Developer Org · Twilio REST API · Apex · Salesforce Flow · Named Credentials · Custom Metadata