Everything to get transactional texts live for a client like a bail bondsman — court-date, check-in and payment reminders. Steps marked YOU need your hands on the Twilio console; DONE is already coded; BUILDING is what I'm writing right now. Tap a step to check it off.
← Back to Labs| Twilio Account SID | AC386411e7f0b20bb01989ca253da7f22b |
| Boom Media number | +1 561-782-8290 (Boom's own / pilot test sender only) |
| Example vertical | Bail bonds — a high-scrutiny vertical (the right stress test); the same flow works for any client |
| Message type | Transactional (reminders) — needs consent + opt-out, but NOT prior opt-in like marketing |
| Shared database | db.boommedia.us (self-hosted Supabase) |
| Send rail (built) | sendSms({ kind:'transactional' }) — Localey/src/lib/sms.ts |
| Reminder feature home | Dashee (service-industry client portal) — building now |
Read this before touching the console — it clears up 90% of the confusion.
In the US you can't just start texting customers from a business number. Carriers (Verizon / AT&T / T-Mobile) require every business to be registered first — a one-time anti-spam step called A2P 10DLC ("business texting on a normal 10-digit number"). Register a client once (~1–3 days to approve) and they can text forever. That registration is the only hard part; everything else is a few clicks.
Three layers of "prove the business is real, then say what you'll text":
Then: buy a phone number → drop it into a Messaging Service (a "sender box" tied to the Campaign) → the number can text.
Fastest way to find anything: use the search bar at the very top of the console and type the word — it jumps you straight there:
A2P | the guided registration (Profile → Brand → Campaign) |
Messaging Services | create the sender + attach your number |
Buy a number | get the phone number |
Trust Hub | your business profiles (the ID-check layer) |
Menu path: left sidebar </> (Develop) → Messaging → Regulatory Compliance → A2P 10DLC. (The gear icon you're on now is Settings — wrong spot.)
The runbook mentions ISV/Reseller and one-subaccount-per-client — that's an optimization for when you have several SMS clients. For your first client, just run the A2P wizard once in your main account and get one number sending. You can reorganize into subaccounts later; it won't block the pilot.
Before anything else — your credentials were exposed in chat.
Same page → Create API key → name it boom-sms → type Standard. Copy the SID (SK…) and Secret (shown once — save it now).
TWILIO_API_KEY_SID / TWILIO_API_KEY_SECRET for exactly this.Console → account switcher (top-left) → View subaccounts. For the reseller model, create/keep one subaccount per client (each client gets its own subaccount).
twilio_subaccount_sid from the sms_clients table.This is the real bottleneck — brand + campaign approval takes ~1–3 business days (longer if bail gets extra vetting). Start it today while the console is open.
In Trust Hub → Registrations: a Campaign attaches to a Brand, and a Brand is built from a Business Profile. So an empty "A2P Campaigns" screen is normal — you need a Brand first. Order:
Console → Trust Hub → your Primary Business Profile → Business Identity → "ISV Reseller or Partner."
Trust Hub → A2P 10DLC → create a Secondary Customer Profile for the client → then a Brand (Standard / Low-Volume Standard, ~$4.50 one-time).
You'll need from the client — get it exactly right:
Under the Brand → create a Campaign (~$15 one-time vetting + a small monthly). Pick the transactional use case — "Account Notification" or "Customer Care", not Marketing.
Campaign description (paste something like this):
Sample messages (must match what you'll really send — carriers compare):
Each client gets their own local number (~$1.15/mo) under their own Messaging Service — never shared. Two ways:
CRON_SECRET). Record the MG… service SID + number on the client's sms_clients row.The send rail is written. These steps turn it on.
Run Localey/supabase/002_sms_shared.sql once against the shared Supabase (Studio SQL editor). It creates sms_clients, sms_consent, sms_messages — no app prefix because they're ecosystem-shared.
One row in sms_clients per client, so the rail knows which number/brand to send from:
insert into sms_clients (client_id, display_name, twilio_subaccount_sid,
messaging_service_sid, phone_number, brand_sid, campaign_sid, registration_status)
values ('<the client's tenant id>', 'Your Client Business Name',
'AC…', 'MG…', '+1561…', 'BN…', 'CM…', 'approved');
client_id decision. It must be the SAME id for this business across every app (so STOP is honored everywhere). For the Dashee reminder feature I'm building, I'll map Dashee's tenant id → this client_id. You just need to use that same id here.In each app's environment (Vercel project settings — or Doppler, which you already provisioned and should finally wire):
TWILIO_ACCOUNT_SID=AC386411e7f0b20bb01989ca253da7f22b TWILIO_API_KEY_SID=SK... # from Phase 1 TWILIO_API_KEY_SECRET=... # from Phase 1 TWILIO_AUTH_TOKEN=... # the NEW rotated token (inbound webhook signature only) TWILIO_FROM_NUMBER=+15617828290 # pilot fallback sender SMS_WEBHOOK_BASE=https://dashee.online # (or localey.online) — must match the webhook you set below CRON_SECRET=... # already set; guards the numbers/dispatch routes
.env.local again.Deploy the app (Vercel, git push). Then on the client's Messaging Service → Integration:
Even transactional texts need consent on file and a working STOP. This is also the proof Twilio asked for.
I'm building this into Dashee now: a contacts + consent capture screen where the client records each customer's mobile number with an SMS-consent checkbox (source, exact wording, timestamp) written to sms_consent via the existing POST /api/sms/consent.
For bail bonds the natural capture point is the indemnitor agreement / application at intake — add the consent line there and record it in Dashee.
Building now in Dashee: from a contact, the client picks a reminder type (court date / check-in / payment), fills the details, and sends now or schedules it. It calls the shared rail:
sendSms({ clientId, phone, body, app: 'dashee', kind: 'transactional' })
The rail handles E.164, STOP suppression, per-client sender routing, segment/cost counting, and the audit log automatically.
Opt your own number in, send yourself a reminder. Confirm it arrives, the row lands in sms_messages, then text STOP and confirm you flip to opted_out and the next send is blocked.
With the campaign approved, consent captured, and the STOP test green — turn it on for your client and send real court-date/check-in reminders. Watch sms_messages delivery statuses for the first batch.
Only after the pilot sends cleanly. Each is its own build.
Rewardee offers, BOO order status, Localey review asks all call the same sendSms() + shared ledger. First lock the canonical client_id mapping (flagged in the migration) so STOP suppresses across every app.
Move the inbound webhook into Replyee, add a thread model, point the RAG engine at inbound texts — "your texts answer themselves at 10pm." This is what justifies the higher tier.
Three tiers ($29/$59/$99) + metered overage. Build a self-serve 10DLC registration form once you have ~5 SMS clients — until then, register them by hand as above.
| Shared ledger SQL | Localey/supabase/002_sms_shared.sql |
| Send rail | src/lib/sms.ts — sendSms(), consent gate, segments, STOP words |
| Inbound + STOP webhook | src/app/api/sms/inbound/route.ts |
| Delivery status webhook | src/app/api/sms/status/route.ts |
| Consent write endpoint | src/app/api/sms/consent/route.ts |
| Number search/buy | src/app/api/sms/numbers/route.ts |
| Reminder feature + consent UI | Dashee — building now |