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
| Twilio | Naive | Notes |
|---|---|---|
require('twilio')(accountSid, authToken) | new Naive({ apiKey }) | Server-side credential in both cases |
| Account SID / Auth Token — one Twilio account, isolated from your other vendors | tenant user via naive.forUser(id) — scopes every primitive | The core consolidation win |
Subaccounts (api.accounts.create) for per-customer isolation | Tenant users (naive.forUser(id)) | Naive’s unit of isolation spans every primitive, not just telephony |
client.availablePhoneNumbers('US').local.list({ areaCode }) → search | (folded into provision) | No separate search step — pass area_code to provision |
client.incomingPhoneNumbers.create({ phoneNumber }) → buy | client.phone.provision({ ein, area_code, label }) → phone.id / phone.e164 | One call buys the number and submits the 10DLC campaign |
| A2P 10DLC Brand + Campaign (TrustHub), Messaging Service, sender pool | Automatic carrier registration inside provision (see campaign.status) | Naive registers the campaign against your formed entity; no Messaging Service to assemble |
client.incomingPhoneNumbers.list() | client.phone.list() | List numbers for the identity |
client.incomingPhoneNumbers('PNxxx').remove() | client.phone.release(phoneId) | Release the number, stop rental billing |
client.messages.create({ from, to, body }) | client.phone.send({ from, to, body }) | from is the UUID on Naive, the E.164 string on Twilio |
client.messages.create({ messagingServiceSid, to, body }) | — | No Messaging Service / sender-pool selection — from is one number |
Inbound via the number’s smsUrl webhook (TwiML POST) | client.phone.messages(phoneId) poll + sms.received webhook | Stored inbound; no synchronous TwiML reply — see gaps |
client.messages.list({ to }) → message logs | client.phone.messages(phoneId, { limit }) → inbound for a number | Cursor-paginated, newest first |
| (read one message in the logs) | client.phone.read(messageId) | Full body + any inbound media URLs |
| API Keys + scopes for least-privilege | Account Kit phone primitive + per-agent assignment (send_sms, receive_sms) | Permission is execution-time policy on the identity — see gains |
statusCallback delivery receipts | Partial — synchronous status on send | No async delivery-status callback stream — see gaps |
Programmable Voice (calls, TwiML <Dial>) | — | Out of scope for the phone primitive |
| MMS send, WhatsApp, RCS, short codes, alphanumeric sender IDs | — | Not provided; inbound media URLs are captured — see gaps |
| Twilio Verify (OTP), Lookup, Conversations | — | No equivalent products |
| International numbers | — | US numbers only today |
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.Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).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.)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.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.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.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.| Twilio feature | Status on Naive | Workaround |
|---|---|---|
Programmable Voice (calls, TwiML <Dial>, IVR) | Not provided (capabilities.voice: false) | None — keep Twilio for voice, or wait for parity |
| MMS send | Not provided (capabilities.mms: false) | Inbound media URLs are captured via phone.read; outbound is SMS text only. Link to Storage instead |
| WhatsApp / RCS / short codes / alphanumeric sender IDs | No equivalent senders | SMS from a single 10DLC long code only |
| International numbers | US numbers only | None today — US 10DLC only |
| Twilio Verify (OTP), Lookup, Conversations | No equivalent products | Build OTP on top of phone.send; Naive’s verification is founder KYC, not phone OTP |
| Messaging Services / sender pools / number pooling | Not provided — from is one number | Provision multiple numbers and pick a from in your own code |
| Self-managed A2P 10DLC (TrustHub Brand/Campaign API, ISV sub-registration) | Automatic, not configurable | provision registers the campaign for your entity; you don’t control the Brand/Campaign objects |
Synchronous inbound reply (TwiML <Response>) | Not provided | Inbound is stored + (optionally) a sms.received event; reply by calling phone.send |
Async delivery-status callbacks (statusCallback) | Partial — synchronous status on send | Inspect the send response; no per-message delivery-receipt stream |
| No formation/EIN required to buy a number | Different model | Naive requires a completed formation + EIN first — the accountability trade |
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