Twilio's Programmable SMS → the Naive phone primitive
client.messages.create(...) to
send and a webhook to receive. It does that job well, and the API is mature. But it is also a
separate vendor account:
- The number lives behind a Twilio Account SID + Auth Token, its own console, and its own billing — disconnected from wherever the agent’s cards, email, secrets, and KYC live.
- A2P 10DLC compliance (Brand + Campaign registration, Messaging Services, sender pools) is yours to wire up and keep registered against your business entity.
- “Who let this agent text a customer, and what else can it touch?” is answered in Twilio for messaging, and in unrelated systems for everything else. The rented DID has no shared accountability with the rest of the agent’s footprint.
phone primitive gives the agent the same capability —
provision a US number, send SMS, read inbound replies — but the number is carrier-registered
(10DLC) against one governed identity:
- The tenant user that owns the number is the same user that owns its cards, its email inboxes, its vault secrets, and its KYC.
- The 10DLC carrier campaign is registered against the entity that user already formed and KYC’d — accountability is built into provisioning, not bolted on.
- Whether an agent may send SMS — and whether provisioning a new number freezes for human approval — is decided by that user’s Account Kit and per-agent assignment at execution time.
Twilio is a trademark of Twilio Inc., used here for identification only. No endorsement or affiliation is implied.
Tested against: the Twilio Node helper library
twilio
v5.x against the Twilio REST API (version path 2010-04-01, base
https://api.twilio.com, docs snapshot June 2026), and the Naive Node SDK
@usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1,
docs snapshot June 2026).Version notes:- Twilio buys a number in two calls (
availablePhoneNumbers(...).local.listto search, thenincomingPhoneNumbers.create) and treats A2P 10DLC registration as a separate flow (TrustHub Brand/Campaign + a Messaging Service). Naive folds search + purchase + carrier registration into onephone.provisioncall. - Twilio identifies a number by its
sid(PNxxxx) and accepts the E.164 string asfrom. Naive returns a number UUID (phone.id) plus thee164string;phone.sendtakes the UUID asfrom. - Naive is US SMS only today. Voice, MMS send, WhatsApp, international numbers, short codes, and Twilio Verify have no equivalent — see what doesn’t map yet.
- Prerequisite difference: Naive requires a completed LLC formation
and the company EIN before the first
phone.provision(the number is registered against your business entity). Twilio has no such requirement. This is the accountability trade — see the gaps section.
Concept map
Before / after: the core path
The path that matters for almost every SMS agent is get a number, then send a message from it. Here it is on both platforms.- One call to provision, not two-plus. Twilio is search → buy → register A2P → attach a
Messaging Service. Naive’s
provisionbuys the number and submits the 10DLC campaign together. fromis a UUID, not the E.164 string. Twilio’sfromis the number itself. Naive’sphone.sendtakes thephone.idUUID; thee164string is returned for display.- Outbound is carrier-gated on both — Naive surfaces it inline. Until the 10DLC campaign is
active, Naive returnscompliance_pending(no charge); pollphone.status()for progress. - The id is your identity, not a separate account. In Twilio the Account SID scopes Twilio.
In Naive the same
forUser(id)handle also owns the agent’s cards, email, vault, and KYC.
Receiving replies
Twilio delivers inbound SMS by POSTing to the number’s configuredsmsUrl (you reply with TwiML).
Naive stores inbound messages against the receiving number; read them by polling, and react in real
time with the sms.received event.
- On Naive, inbound texts are captured automatically and queryable via
phone.messages/phone.read— that path always works the moment a number is live. - The
sms.receivedwebhook is documented in the phone primitive for real-time reactions. The canonical emitted-event list isGET /v1/webhooks/event-types— treat that as the source of truth and confirm the event before depending on it (see gaps). - There is no synchronous TwiML reply. To answer an inbound message, call
phone.send.
Minimal viable migration
The smallest swap that keeps a working agent running is just provision + send + read inbound.1
Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).2
Clear the provisioning prerequisites (one-time)
A Naive number is carrier-registered against your business entity, so before the first
provision you need a completed LLC formation and the company
EIN. If you do not have one, run KYC →
formation first. (Twilio has no equivalent gate — this is the
accountability trade-off; see gaps.)3
Swap number acquisition
Replace
availablePhoneNumbers(...).local.list(...) + incomingPhoneNumbers.create(...) with a
single client.phone.provision({ ein, area_code, label }). Keep the returned phone.id. Drop
the separate A2P 10DLC Brand/Campaign and Messaging Service setup — provision submits the
campaign for you.4
Swap send
Replace
client.messages.create({ from, to, body }) with
client.phone.send({ from: phone.id, to, body }). Use the UUID as from. Handle
compliance_pending by retrying once phone.status() shows the campaign active.5
Swap inbound
Replace your
smsUrl TwiML handler with client.phone.messages(phone.id) /
client.phone.read(id) (and optionally the sms.received webhook).
Reply by calling phone.send instead of returning TwiML.6
Ship it
At this point you are off Twilio for the core US-SMS send/receive path. Everything below is
upside, not a requirement.
Consolidate further once you’re on Naive
This is where the migration pays for itself. In Twilio, the Account SID isolates telephony and nothing else, and the number is an anonymous rented DID. On Naive, the unit of isolation is a tenant user, and the number is rooted in that identity’s verified, formed business.Gain #1 — one identity across primitives
- With Twilio, the agent’s numbers are an island behind an Account SID. With Naive,
naive.forUser(acme.id)is a single handle to phone and cards and email and vault and KYC. - The number is not an anonymous DID — its 10DLC campaign is registered against the entity that identity already formed and verified. Tear the customer down from one place; sibling tenants can never read each other’s messages, cards, or secrets.
Gain #2 — execution-time permission enforcement
- Whether an agent may send SMS at all is per-agent assignment on the number, and whether provisioning a new number freezes for human review is policy on the Account Kit — not a check you hand-write and not a Twilio API-Key scope you manage separately.
- The agent’s code is identical for every tier —
client.phone.send({ ... }). Whether the call runs is decided at execution time:- An agent without
send_smson the number is refused withforbidden, with no code change on your side. phone.requiresApproval: truefreezes a provision until a human approves — the API replays it only after approval. Sending SMS is a routine action and is not approval-gated.
- An agent without
Gain #3 — unified accountability
- Every provision, send, and inbound reply for a customer lands in one per-user activity log — alongside their card, email, vault, and KYC events, not in a separate Twilio console:
- That is the question that is hard to answer when SMS lives in Twilio, cards live in Stripe, and secrets live somewhere else. Under Naive it is a single query.
What does not map yet
A migration guide that hides gaps is worse than none. The core path (provision → send → read inbound) maps cleanly for US SMS, but the following Twilio capabilities have no direct equivalent on Naive’s phone primitive today. Check this list against your app before you commit.Where to go next
phoneprimitive — full provision/send/receive lifecycle + 10DLC gatingphoneSDK sub-client — typed method signaturesphoneCLI — provision, send, messages, assign, release- Webhooks — outbound event subscriptions (confirm
sms.receivedviaGET /v1/webhooks/event-types) - Account Kits and Approvals — the policy model behind execution-time governance and provision approval
- Verification and Formation — the KYC + entity that the carrier registration (and the consolidation) hangs off
- Tenant users — the identity that owns the number, cards, email, and vault
Related reading (blog)
- Why consolidate agent infra on one governed identity — migration thesis behind this guide
- Building AI Agents Into Your SaaS — tenant user anchor for every migration
- How To Build An Agentic Appointment Reminder Agent — SMS reminder tutorial