The SMS conversations that never became tickets
It is the second Tuesday of the month and the support lead is sitting with the head of RevOps, both looking at the same spreadsheet that nobody trusts. The support lead has Zendesk Explore open on the left screen: 4,180 tickets last month, first reply time holding at 2 hours 11 minutes, CSAT at 91. Fine numbers. The RevOps screen has the Twilio Console open, Messaging Insights, and a different set of numbers: 61,400 inbound SMS segments to the support long code, which is roughly ten times the number of tickets, and nobody in the room can say what most of those messages were.
The question on the table is not about volume. It is this: how many of those inbound texts were a customer with a real problem that never got logged, never got a ticket number, never counted against a queue, and possibly never got resolved? The support lead's instinct is "almost none, the agents handle SMS in the shared inbox." The RevOps instinct is "then why did three escalations last quarter start with a text message we had no record of?"
Neither screen answers it. Not because either product is missing a report, but because each one holds a different half of the evidence, and one of those halves is not a record at all.
What Twilio shows on its own
Twilio's message logs are excellent at what they are. For every inbound and outbound message you get the message SID, the from and to numbers, direction, status, error code if any, the timestamp to the second, the segment count, the price, the messaging service and the carrier response. Messaging Insights aggregates that into delivery rates, error breakdowns and response-time distributions across a date range. If a message failed with a 30007 filtering block, Twilio will tell you exactly that, and it will tell you which carrier did it.
Twilio also stores the body, subject to your retention settings, and you can pull it via the API. So it is not true that Twilio has no content. It has the text.
Where it stops is meaning. Twilio knows that +1 415 555 0142 sent nine inbound messages across four days and received six outbound. It does not know that this was one customer, escalating, about a duplicate charge. It has no concept of a conversation being open or closed, no idea whether a human replied or an autoresponder did, and no notion of resolution at all. The word "resolved" does not exist in Twilio's data model, because Twilio is a transport layer and resolution is not a transport property. Twilio can tell you a message was delivered. It cannot tell you it was answered.
What Zendesk shows on its own
Zendesk holds the other half and holds it well. Every ticket has a requester, a channel, tags, a group, an assignee, a status, a satisfaction rating, and a full comment thread with timestamps for each public and internal reply. Explore will slice all of it: tickets by channel, first reply time by group, reopen rate, backlog by age. If the Twilio SMS channel integration is installed, texts that came in through it become tickets with a channel value you can filter on, and those tickets are as measurable as any other.
Where Zendesk stops is the messages that never became tickets. Explore reports on the population of tickets. A text that arrived at the support number, got handled by an agent from their phone, or got a canned autoreply, or got nothing, produces no row. It is not undercounted in Zendesk, it is absent. Zendesk cannot report on the negative space around its own dataset, and it would be strange to expect it to.
| Twilio | Zendesk | |
|---|---|---|
| Every inbound SMS to the number | yes | only if it became a ticket |
| Message body text | yes | yes, inside the ticket thread |
| Who the customer is | phone number only | full requester record |
| Whether a human replied | no | yes |
| Whether it was resolved | no concept | status and CSAT |
| Messages that never became tickets | present, unlabelled | not in the dataset |
Why the gap is structural, not a missing feature
Line up the two halves and the asymmetry is obvious. Twilio's half is a record: structured, keyed, countable, with a SID and a timestamp and a status field that means one of a fixed set of things. Zendesk's half, for the question we are asking, is not a record. It is a sentence somebody wrote.
Because the actual question, "which SMS conversations should have been tickets," reduces to a judgement about content. A text reading "on my way, be there in ten" is not a support issue. A text reading "hey it's still doing the thing, charged twice again" is one, and no field on the Twilio message row distinguishes them. The distinguishing information lives in a free-text body that a human typed at 11:40pm from a car park. It has no schema. It will never have a schema.
This is why the automation platforms sitting next to both products do not close it. Zapier, Make and Twilio Studio are all genuinely capable, and any of them can watch for an inbound SMS and create a Zendesk ticket from it. But they are trigger based: they fire when an event happens and move a record forward from there. They are built to make the next thing happen, not to look backwards over sixty thousand messages that already arrived and ask which of them mattered. A trigger that ran, ran. A trigger that never existed left nothing behind to query.
And the BI tools do not close it either, for a cleaner reason. Looker Conversational Analytics and Power BI Copilot are strong at what they do, and if you land Twilio logs and Zendesk tickets in a warehouse they will happily count both, join on phone number, and chart the difference. What they cannot do is read the message body and decide it was a support issue. Their world is databases and modelled sources: dimensions, measures, defined relationships. A sentence somebody wrote is not a dimension. It is outside the class of thing they can see.
What the join actually requires
Two pieces, and they are different in kind.
The key is the easy one. Twilio gives you the from number, in E.164. Zendesk gives you the requester's phone field, and for tickets created through an SMS channel it gives you the source number directly. So you match on normalised phone number within a time window: an inbound text at 14:02 belongs to a ticket created between 14:02 and, say, 14:02 plus thirty minutes, from the same number. Twilio conversations also need grouping first, since a customer's five texts in eight minutes are one conversation, not five, and the gap threshold for that is a choice you make, not a fact the data hands you.
The hard piece is the judgement. Once you have the unmatched conversations, the ones with no ticket inside the window, somebody has to read them and separate the ones that were a support issue from the ones that were a delivery confirmation, a wrong number, a two-word thanks, or an appointment reply. That is reading, not matching. No join condition, no regex over keywords, no tag will do it reliably, because the same words mean different things depending on what came before them in the thread.
Answering it with Skopx
Skopx sits over both accounts, along with the rest of the nearly 1,000 tools it connects to, and the way you ask is just to ask. In chat, somebody types something like:
Pull every inbound SMS to our support number last month from Twilio, group them into conversations, then check Zendesk for a ticket from the same phone number within 30 minutes. For the ones with no ticket, read the messages and tell me which were actual support issues. Show me the top ones by how serious they look.
What comes back is a count and a list. Say 812 unmatched conversations, of which 190 read as genuine support issues, with the messages quoted, the phone numbers, the timestamps, and a citation on each one back to the Twilio message SID and, where relevant, the Zendesk ticket it should have matched. The judgement is legible because you can see the sentence it was made from, and if you disagree with a call you can see exactly which text it was reading.
Once the shape of the answer is right, you can ask for it as a standing console instead of a chat thread you have to re-run. That is what Internal Apps is: you describe the view in a sentence and Skopx builds it. A monthly console here would show unmatched conversation count against ticket count, a list of the flagged conversations with the message text in place, filters by date and by number, and one action per row: a button an agent clicks to create the Zendesk ticket, with a confirmation step before it writes anything. Reading is automatic. Writing waits for a person.
What this does not do
It does not watch your SMS traffic in real time and it does not alert you. There is no scheduled run, no trigger, no background job creating tickets while you sleep. If you want inbound texts turned into tickets automatically at the moment they arrive, that is a job for Twilio Studio or Zapier, and you should use them for it. This is the retrospective view, run when you ask for it.
It stores nothing. No forms, no new records, no separate database of conversations. It reads from Twilio and Zendesk each time and the only write is the button somebody presses.
The classification is a model reading text, so it will be wrong sometimes. It will flag a chatty non-issue and it will miss a terse real one. That is why the message body sits on the row: the console is built to be overruled, not obeyed. Treat the number as a starting point for a conversation with your support lead, not as a metric to put in a board deck.
And it only sees what the connected accounts expose. If your Twilio retention settings have already dropped message bodies, or if support texts arrive on a number outside the account, those conversations are invisible here, exactly as they are everywhere else.
Skopx Team
The Skopx engineering and product team