SMS agent quickstart
This is the shortest path from zero to a sent message over MCP. There are two compliance paths; toll-free is the fastest and the recommended default (one form, no fees, no brand/campaign). Pick 10DLC only when you specifically need local numbers or higher throughput.
Whatever path you take, get_sms_setup_status is the source of truth. After every step, call it: it returns the unified state, the reason for any hold, the concrete fix, the exact next tool to call, and canSend (whether production sends are allowed yet). Poll it while approvals are pending — don’t guess from individual objects.
All SMS tools return
sms_unavailablewhen Telnyx isn’t configured on the platform. Role gates match the REST surface: buying numbers and compliance submissions needmanage(owner/admin); wiring and sending needdeveloper+.
Path A — Toll-free (fastest, free)
purchase_phone_number(numberType: "toll_free")
→ submit_tollfree_verification
→ poll get_sms_setup_status until canSend == true
→ send_sms
-
Buy the number.
{ "name": "purchase_phone_number", "arguments": { "e164": "+18445550123", "numberType": "toll_free" } }If the cost is over your approval threshold (or you’re not an owner), you get an
approvalUrlinstead — surface it and wait for approval, then the purchase completes. -
Submit the verification. One form: business + contact details, the numbers to verify, use case, sample/production message content, the opt-in workflow plus at least one opt-in evidence URL (screenshots must show consent checkboxes UNCHECKED by default), and message volume. Required carrier columns the tool validates up front:
entityType(legal entity type), thebusinessRegistrationNumber/Type/Countrytriplet (EIN +"US"for a US business), andprivacyPolicyUrl+termsAndConditionsUrl— these are DEDICATED fields; describing them inadditionalInformationdoes not populate them. Full state name ("California", not"CA") and a non-emptyadditionalInformationare required quirks — the tool validates locally and returnserrorsif anything is missing, so fix and resubmit for free.{ "name": "submit_tollfree_verification", "arguments": { "phoneNumbers": ["+18445550123"], "businessName": "…", "entityType": "PRIVATE_PROFIT", "businessRegistrationNumber": "12-3456789", "businessRegistrationType": "EIN", "businessRegistrationCountry": "US", "privacyPolicyUrl": "https://…/privacy", "termsAndConditionsUrl": "https://…/terms", "useCase": "…", "optInWorkflowImageUrls": ["https://…/optin.png"], "messageVolume": "…", "additionalInformation": "…" } } -
Poll status.
get_sms_setup_status(numberId)movespending_review → approved. Typical turnaround ≈5 business days. Resubmission is free and unlimited (except a terminal “High Risk - Fraud” rejection). -
Send. Once
canSendis true,send_sms. Before then,allowPending: truegets a send accepted but not delivered — carriers reject pre-approval toll-free traffic (Telnyx40329) and the message finalizesdelivery_failed. Use it to smoke-test your wiring, never to reach a real recipient. Andsentis not proof of delivery: polllist_sms_messagesfor the finalstatus+errorCode.
Path B — 10DLC local
register_sms_brand ← get_sms_brand reads it back (brandId)
→ UNVERIFIED? read failureReasons → update_sms_brand (fix in place + re-vet)
→ (sole-prop only) confirm_sms_brand_otp (resend_sms_brand_otp for a new PIN)
→ create_sms_campaign ← $15 per submission — validate first; list_sms_campaigns first if one may exist
(approval-gated: poll get_approval(approvalId) → completion.result.campaignId)
→ assign_number_to_campaign ← campaignId from list_sms_campaigns / get_sms_setup_status
→ poll get_sms_setup_status until canSend == true
→ send_sms
-
Register the brand (once per org — the org’s legal identity).
- Standard:
entityType(LLCs and corporations arePRIVATE_PROFIT) +companyNameandeinexactly as printed on the IRS CP-575 / 147C letter (punctuation and LLC/Inc. count — this is not your display name) + the address the IRS has +email,phone,vertical. TCR verifies within minutes. - Sole proprietor:
entityType: "SOLE_PROPRIETOR", no EIN —firstName/lastName+ USmobilePhone+ address. The PIN is texted automatically (otpRequired: true, expires in 24 h). Caps: 1 campaign, 1 number, ~1,000 msgs/day. - Formatting is normalized for you (EIN dashes, “utah” →
UT, phone formats). Every problem comes back at once as field-namederrors;warningsflag likely TCR trouble (e.g. a legal name with no LLC/Inc.).
{ "name": "register_sms_brand", "arguments": { "entityType": "PRIVATE_PROFIT", "displayName": "Acme", "companyName": "Acme Widgets, LLC", "ein": "12-3456789", "email": "ops@acme.com", "phone": "+18015550123", "website": "https://acme.com", "vertical": "TECHNOLOGY", "street": "1 Main St", "street2": "Suite 4", "city": "Vernal", "state": "UT", "postalCode": "84078" } }- If the brand comes back UNVERIFIED,
get_sms_brand/get_sms_setup_statuscarryfailureReasons— TCR’s own words and the exact fields (e.g.TAX_ID→ein,companyName,entityType) — plussubmittedFieldsshowing what was compared. Correct only those fields withupdate_sms_brand(confirm: true): the brand is fixed in place and re-vetted. Do not delete and re-register. Telnyx allows one re-vet after registration and then one per 3 months (revetAvailableAt).
- Standard:
-
Confirm the OTP (sole-prop only):
{ "name": "confirm_sms_brand_otp", "arguments": { "brandId": "…", "pin": "123456" } }PIN expired or never arrived?
resend_sms_brand_otp(brandId). Lost thebrandId?get_sms_brandreturns it as a top-levelbrandId(and again asbrand.id); so doesget_sms_setup_statuson any number, under the samebrandIdkey — never ask an operator to look it up. If Telnyx can’t automate this, the status degrades toaction_requiredwith instructions rather than blocking — checkget_sms_setup_status. -
Create the campaign — mind the $15. Each
create_sms_campaignsubmission (and every resubmission) costs $15. The tool validates the whole payload locally first and returnserrorswith no charge if anything fails TCR rules — never spend money on a payload you haven’t cleared. Over your approval threshold it returns anapprovalId+approvalUrl; the charge happens on approval. Then pollget_approval(approvalId):statusmoves toapproved, andcompletion.result.campaignIdis the idassign_number_to_campaignneeds — orcompletion.status: "failed"tells you nothing was created (re-runcreate_sms_campaign; it requests a fresh approval, never a dead one). Lost track of a campaign?list_sms_campaignslists them withcampaignIdandassignedNumberIds, andget_sms_setup_statuson a number whose brand already has a live campaign saysassign_number_to_campaignwith thatcampaignIdinstead of steering you into another $15 submission.{ "name": "create_sms_campaign", "arguments": { "brandId": "…", "usecase": "ACCOUNT_NOTIFICATION", "description": "Acme sends billing and account notices to customers who opted in at signup.", "messageFlow": "Customers tick an unchecked consent box next to their mobile number at https://acme.com/signup.", "sampleMessages": ["Acme: your invoice is ready. Reply STOP to opt out.", "Acme: new sign-in on your account. Reply HELP for help."], "privacyPolicyLink": "https://acme.com/privacy", "dryRun": true } }What carriers check first — and what the tool now always sends — are the subscriber opt-in / opt-out / help attestations plus the opt-in, STOP and HELP reply messages; leave the replies out and they are generated from the brand (name + contact). Rules enforced locally: 2–5 distinct samples, at least one naming the brand and one with opt-out language;
subUsecasesforMIXED(2–5) andLOW_VOLUME(1–5);SOLE_PROPRIETORonly with a sole-proprietor brand. Run withdryRun: trueto see the resolved campaign andwarningsfree, fix anyerrors, then dropdryRunto submit.- Rejected?
list_sms_campaigns/get_sms_setup_statuscarryfailureReasonsandproviderStatus.TCR_FAILED→ fix the named fields and submit a new campaign ($15).TELNYX_FAILED/MNO_REJECTED→appeal_sms_campaign(optionally with replacement samples) — no new submission.
- Rejected?
-
Assign the number.
assign_number_to_campaign(campaignId, numberId)— enforces the 49-numbers-per-campaign T-Mobile cap. -
Poll status.
get_sms_setup_statuswalks TCR review → per-carrier MNO review (T-Mobile ≤24 h; AT&T/Verizon 1–3 business days). -
Send once
canSendis true.
Wire it into an app
Once a number can send, attach it to a service to inject the SMS_API_* env vars and let the app send/receive on its own:
{ "name": "attach_phone_number_to_service", "arguments": { "numberId": "…", "serviceId": "…" } }
{ "name": "set_sms_inbound_webhook", "arguments": { "numberId": "…", "url": "https://app.example.com/sms/inbound" } }
Need the injected values for a local .env or a runtime outside Tandem? reveal_sms_credentials(numberId) reads back SMS_API_URL / SMS_API_KEY / SMS_PHONE_NUMBER / SMS_WEBHOOK_SECRET per attached service. list_env_vars only previews them, and the attach response shows the key once.
The inbound URL is inbound-only. Pass optional statusUrl on the same tool to subscribe to terminal outbound delivery status (sms.status); otherwise poll list_sms_messages for a message’s terminal status + errorCode.
See the SMS & phone numbers guide for the SMS_* env contract, the send API, and inbound signature verification.
Cheat sheet
- Start and re-check with
get_sms_setup_status— it names the next tool, carries thebrandIdon the 10DLC path, and tells youcanSend. - Toll-free is fastest and free. Prefer it unless you need local presence / higher throughput.
- 10DLC campaigns cost $15 per submission. The tool validates locally first — never submit a payload with
errors; usedryRun: trueto preview. - Rejections are fixable in place: brand →
update_sms_brand, campaignTELNYX_FAILED/MNO_REJECTED→appeal_sms_campaign. The reasons are always infailureReasons. - Sends are blocked until compliance is
approved; the send error carries thesetupStatusso you know why.allowPendingbypasses the gate, not the carriers. - You never need platform staff to read a value to you.
get_sms_brandfor the brand id,reveal_sms_credentialsfor the injectedSMS_*values.