Advanced Twilio: Inbound SMS, Delivery Tracking, and Two-Way Flows
Go beyond sending: receive inbound SMS, track delivery status with callbacks, build two-way messaging flows, and handle opt-outs properly.
One-way SMS — your system sends, customers read — is already valuable. But the real power of Twilio comes when you close the loop: your system sends, customers respond, your system processes the reply and acts on it.
This article covers everything that happens after you can reliably send. Delivery tracking so you know which messages actually reached people. Inbound SMS so customer replies trigger real automation logic. Two-way flows for confirmation-based workflows. Opt-out handling because TCPA compliance is not optional. And A2P 10DLC registration because skipping it will get your messages filtered out.
Status Callbacks: Knowing If Your Messages Delivered
The basics article explained that “sent” and “delivered” are different statuses. “Sent” means Twilio handed the message to the carrier. “Delivered” means the carrier confirmed it reached the recipient’s device. You want to know about both — especially failures.
Without status callbacks, you’re flying blind. You send messages and hope they arrive. With status callbacks, you have a real record: this customer received the job confirmation, this one’s message failed because of an invalid number, this one’s delivery was undelivered (and you should follow up differently).
Configuring the status callback:
There are two ways to set this up. The first is on the phone number itself — any message sent from that number will use the callback. The second is passing a StatusCallback parameter on each individual API request.
For a clean architecture, configure it at the number level. In Twilio:
- Go to Phone Numbers → Active Numbers → click your number
- Scroll to “Messaging Configuration”
- In the “A Message Comes In” section, set your webhook URL — this handles inbound SMS (covered next)
- Under “Primary Handler Fails” or via the API, set the
StatusCallbackURL
For the most reliable setup, pass the StatusCallback parameter in your n8n Twilio node’s “Additional Fields.” Set it to your n8n webhook URL. Then every outbound message from n8n automatically reports its delivery status back.
Building the n8n status callback handler:
Create a new workflow in n8n with a Webhook trigger. Set the method to POST. Copy the webhook URL and paste it as your Twilio StatusCallback URL.
Twilio’s callback payload contains:
MessageSid— the unique message IDMessageStatus— the current status (sent, delivered, failed, undelivered)To— the recipient numberFrom— your Twilio number
In the workflow after the Webhook node, add a Supabase node to update your notification log:
Operation: Update
Table: notifications_log
Update Key: twilio_message_sid
Fields:
status: {{ $json.body.MessageStatus }}
delivered_at: {{ $json.body.MessageStatus === 'delivered' ? $now : null }}
This requires your notifications_log table to have a twilio_message_sid column that you populate when you first create the log entry. When you send an SMS via n8n, the Twilio node returns the message SID in its output. Save that SID to Supabase at send time. The status callback can then find and update the right record.
The result: every message in your database has an accurate delivery status. You know exactly which customers have been reached and which haven’t.
Receiving Inbound SMS
When a customer replies to your Twilio number, Twilio fires a webhook to a URL you configure. That URL is your n8n workflow. The reply becomes data you can act on.
Configuring your Twilio number for inbound:
In Twilio → Phone Numbers → Active Numbers → click your number:
- Under “A Message Comes In”: set it to “Webhook,” method “POST,” and enter your n8n webhook URL
Building the inbound handler in n8n:
Create a workflow with a Webhook trigger node. Set Method to POST. Set the path to something descriptive like /twilio/inbound.
The payload Twilio sends for an inbound message includes:
{
"MessageSid": "SMxxxxxxxx",
"From": "+15551234567",
"To": "+15559876543",
"Body": "On my way",
"NumMedia": "0"
}
The key fields: Body (the message text) and From (the sender’s phone number).
After the Webhook node, add a Set node to extract what you need:
sender_phone→{{ $json.body.From }}message_body→{{ $json.body.Body.trim().toUpperCase() }}
I always normalize the body to uppercase and trim whitespace immediately. Customers type “yes,” “YES,” “Yes ” — normalizing it means your IF conditions work reliably.
Then look up the customer in Supabase using the sender_phone to know who replied. From there, your workflow logic takes over.
Building a Two-Way Messaging Flow
Here’s a real end-to-end example from SendJob: a job completion confirmation flow.
The scenario: When a technician marks a job complete, the system sends the customer an SMS asking them to confirm receipt. If they reply YES, the job record is marked customer-confirmed. If they reply NO, the system flags it for follow-up.
Step 1: Outbound — send the completion SMS
In your job completion workflow (triggered by a Supabase database webhook when job status changes to “complete”):
- Supabase node: query the job record including customer phone
- Twilio node: send SMS
- From: your Twilio number
- To:
{{ $json.phone_e164 }} - Body:
Your job #{{ $json.job_ref }} is complete. Reply YES to confirm you're satisfied, or NO if there's an issue. — SendJob
- Supabase node: insert a row into
notifications_logwith thetwilio_message_sid,job_id,notification_type: 'completion_confirmation', andawaiting_reply: true
Step 2: Inbound — process the reply
In your inbound SMS handler workflow:
- Webhook trigger (Twilio fires this when the customer replies)
- Set node: extract
sender_phoneand normalizemessage_bodyto uppercase - Supabase node: look up the customer by phone number, get their
id - Supabase node: find the most recent open notification log entry for this customer where
awaiting_reply = trueandnotification_type = 'completion_confirmation' - IF node: does
message_bodycontain “YES”?
If YES branch:
- Supabase: update the job record —
customer_confirmed = true,confirmed_at = now() - Supabase: update the notification log —
awaiting_reply = false,reply = 'YES' - Twilio: send a thank-you SMS:
Thanks for confirming! Your invoice will arrive shortly.
If NO branch:
- Supabase: update the job record —
customer_flagged = true - Supabase: update the notification log —
awaiting_reply = false,reply = 'NO' - Twilio: send an acknowledgment:
We're sorry to hear that. A team member will be in touch shortly. - n8n Send Email node (via Resend): notify the operations team that a customer flagged a job for follow-up
That’s a complete two-way flow. The outbound message creates a pending state in your database. The inbound reply resolves that state and triggers the next action. No human intervention required unless the customer says NO.
Handling Opt-Outs
This section is not optional. TCPA (the Telephone Consumer Protection Act) governs commercial SMS in the US, and violations can result in serious fines. The good news: compliance isn’t hard if you handle it correctly from the start.
How Twilio handles STOP automatically:
When a recipient replies STOP (or STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT), Twilio automatically adds their number to an opt-out list for your number. Future messages from your Twilio number to that recipient will be blocked by Twilio — the messages won’t be delivered, and you’ll receive an error status.
Twilio also automatically sends a reply acknowledging the opt-out. You don’t need to configure this.
What you still need to do:
Twilio blocking the number doesn’t update your database. If you try to send to that number again through n8n, Twilio will block it, but you’ll still be attempting the send and getting failures. More importantly, you have no record in Supabase that this customer has opted out. Your CRM data is out of sync.
Build this inbound handler in your inbound SMS workflow (as a branch in the IF logic, before your normal reply processing):
IF message_body === 'STOP' (or contains common opt-out words):
→ Supabase: UPDATE customers SET sms_opted_out = true, opted_out_at = now() WHERE phone = sender_phone
→ (No further SMS back to this customer — Twilio already sent the confirmation)
And add a check at the beginning of any outbound SMS workflow:
Supabase: SELECT sms_opted_out FROM customers WHERE id = customer_id
IF sms_opted_out = true → stop the workflow, do not call Twilio
This two-part approach means:
- Your database correctly reflects opt-out status
- You never even attempt to send to opted-out customers
- If Twilio’s opt-out list ever gets out of sync with yours, you’re still safe because you check your own database first
Add a corresponding sms_opted_in flow for START/UNSTOP replies — Twilio re-enables sending automatically, but your database should reflect the re-opt-in too.
TwiML
TwiML (Twilio Markup Language) is XML that tells Twilio how to handle an inbound call or message. You return TwiML from a webhook endpoint and Twilio follows the instructions.
For basic automation workflows using n8n, you rarely need TwiML directly. n8n handles the outbound side, and inbound webhooks are processed as plain HTTP payloads.
Where TwiML becomes relevant:
Auto-reply SMS: Instead of processing an inbound message in n8n and then calling Twilio again to reply, you can return a TwiML <Message> response directly from the webhook. Twilio parses it and sends the reply immediately.
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your reply. We'll be in touch shortly.</Message>
</Response>
In n8n, you can return this from a Webhook node by setting the “Respond With” option to “First Incoming Item’s Binary Data” or using a Respond to Webhook node with XML content type.
IVR phone trees: For voice calls, TwiML is how you build “press 1 for sales, press 2 for support” systems. This is a deeper topic beyond what most automation builders need, but the mechanism is: Twilio calls your webhook URL when a call comes in, you return TwiML with <Gather> and <Say> instructions, Twilio follows them.
TwiML is worth knowing exists. You’ll reach for it occasionally.
Twilio Verify (2FA)
If your application has any kind of user login, two-factor authentication via SMS significantly reduces account takeovers. Twilio Verify is a dedicated product for exactly this.
Here’s how it works:
Step 1: Send a verification code
From n8n (or your backend), call the Twilio Verify API:
POST https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications
Body: To=+15551234567&Channel=sms
Twilio sends a 6-digit OTP to the phone number. The code is valid for 10 minutes by default.
Step 2: Verify the code
When the user enters the code, call:
POST https://verify.twilio.com/v2/Services/{ServiceSid}/VerificationCheck
Body: To=+15551234567&Code=123456
Twilio returns status: "approved" if the code is correct and not expired, or status: "pending" if it’s wrong.
In n8n, use an HTTP Request node for both calls (Twilio Verify isn’t in the native n8n Twilio node). Pass the response back to your application logic, and if status is “approved,” update the user record in Supabase:
Supabase: UPDATE users SET phone_verified = true, verified_at = now() WHERE id = user_id
You need a Twilio Verify Service SID — create one in the Twilio console under Verify → Services. The pricing is $0.05 per successful verification (not per attempt — only successful ones cost anything).
A2P 10DLC Registration
If you’re in the US and sending business SMS at any real volume, this is mandatory. Carriers (AT&T, Verizon, T-Mobile) require businesses sending application-to-person (A2P) messages to register their brand and messaging campaign. Messages from unregistered numbers are increasingly filtered or blocked.
What A2P 10DLC is:
A2P 10DLC is the industry framework for registering 10-digit long-code (regular local numbers) for business SMS use. You register your business as a “brand” and your specific SMS use case as a “campaign.” Carriers use this information to allow legitimate business traffic through and filter out spam.
How to register in Twilio:
In Twilio console → Messaging → Regulatory Compliance → US A2P 10DLC:
-
Register your Brand: Submit your business information — legal name, EIN, business type, website, contact details. Brand registration costs $4 as a one-time fee. Approval typically takes 1–3 business days.
-
Create a Campaign: A campaign describes your specific SMS use case. For SendJob, this would be a “Customer Care” or “Transactional” campaign covering job notifications, appointment reminders, and service communications. Campaign fees are $10–$15 one-time depending on use case, plus a small monthly recurring fee ($1–$2/month). Campaign review takes 1–5 business days.
-
Assign your number to the campaign: After the campaign is approved, link your Twilio phone number to it.
What happens if you skip it:
Your messages will increasingly get filtered. Carriers have been tightening enforcement — what worked without registration 18 months ago often doesn’t work today. You’ll see undelivered messages, reduced deliverability, and eventually outright blocking. The registration process is not difficult. Do it before you go live.
Timing:
Don’t wait until launch day. Start the registration process as soon as you have your Twilio number and a clear idea of your use case. The approval queues can take up to a week in some cases.
Debugging Failed Messages
When Twilio messages fail, the logs tell you why. Here’s how to read them.
In Twilio console → Messaging → Logs → Messages, find the failed message. Click into it to see the error code. Then go to twilio.com/docs/errors and search for that error code.
Common failure reasons and fixes:
Error 21211 — Invalid ‘To’ phone number:
The phone number format is wrong. Confirm it’s in E.164 format (+15551234567). This is almost always the issue when an integration works for some numbers and fails for others.
Error 21610 — Message cannot be sent to the ‘To’ number — the number is unsubscribed: The recipient replied STOP and is on your opt-out list. You should not be attempting to send to them. Fix your opt-out check logic.
Error 21612 — The ‘To’ phone number is not currently reachable: The carrier couldn’t deliver to the handset. Could be a temporary issue (phone is off, out of service area) or a permanent one (number no longer exists). For permanent failures, update the phone record in Supabase to flag it as unreachable.
Error 30007 — Message delivery failed — Carrier violation: Carrier filtering. Most common cause: sending from an unregistered number. A2P 10DLC registration fixes this. Also check message content — URLs, certain phrases, and excessive capitalization all increase spam scores.
Error 30008 — Message delivery failed — Unknown error: Catch-all carrier failure. Could be temporary. Retry logic in n8n (add a Wait node + retry on the same message after a delay) handles transient failures.
The Twilio debugger:
For intermittent issues, go to Monitor → Debugger. This shows all errors and warnings across your account with context — timestamp, which phone number was involved, and what the system was trying to do. It’s much faster than scrolling through message logs when you’re hunting for a pattern.
Next up: What Is Resend? → — the email layer that works alongside your SMS communications.