The link on the message is not yours to open
When a customer sends a photo over WhatsApp or SMS, the picture does not arrive as a file. What arrives, and what gets stored on the ticket comment, is a link belonging to the messaging provider.
That link is not something a browser can simply open. A Twilio media URL is protected by the account credentials that received the message: clicking it without them returns a refusal, not the photo. Meta's image links carry their own token instead, with their own rules about who may follow them.
This matters more than it sounds, because on WhatsApp a photo with no caption is frequently the entire message. A receipt, a screenshot of an error, a picture of the damaged item. If the agent cannot open it, the customer has effectively not been answered, no matter how quickly someone typed a reply.
Handing the raw link to the agent's browser cannot fix this without also handing over the account credentials, which is not a trade anyone should make. So the fetch happens somewhere else.
The server fetches it, and only the bytes come back
Your ticket screen does not link to the provider. It links to an endpoint on your own dashboard, addressed by comment and attachment index, and that endpoint does the fetching on the server before streaming the result to you.
The agent interface is only told three things about each attachment: its position in the message, its content type, and whether that content type is an image. It is never given the provider URL. That is deliberate on two counts. The URL would be useless to the browser anyway, and provider media identifiers are worth keeping out of a page that agents can view source on.
The practical effect is the one you want. An image renders inline in the conversation, a document offers itself as a download, and neither requires the agent to know anything about which messaging provider the customer used.
The URL is never taken from whoever asked
Fetching a web address that arrived from outside your system is the classic shape of a server-side request forgery bug, and this is written as one that knows it.
The endpoint never accepts a URL. It accepts a comment identifier and an index, then reads the stored attachment record itself. That read is scoped by site as well as by identifier, so it is a tenant boundary rather than a filter: an agent guessing comment numbers cannot reach another account's media, because the row simply will not match.
The site is not taken from the request either. It is derived from the signed-in account. A request without a valid dashboard session is refused before any of this runs, which means the endpoint is not a public image host that happens to sit on your domain.
An exact host list, and exactly one redirect
Even with the URL coming from your own database, it is still a value that a messaging provider wrote. So before anything is fetched, the address has to be secure HTTP and its host has to match one of three names exactly. Not a suffix match, not a domain-ends-with test, which is the check that so often turns out to accept an attacker-controlled subdomain of something that merely looks similar.
Redirects are not followed automatically, which is unusual and correct here. Twilio answers with a redirect to its content network, so exactly one hop is permitted, and the destination of that hop has to be on its own separate allow list. Credentials are not sent again on that second request: the redirect target is already a signed URL, and forwarding the account token to another host would leak it for no benefit.
The request is bounded in the ordinary ways too. There is a fifteen second timeout on both connecting and reading, and a twenty five megabyte ceiling on the response, past which the transfer is abandoned rather than buffered.
Why some attachments open and others download
Whether an attachment displays in place or arrives as a download is not a preference. It is a safety rule.
A fixed set of content types is served as itself: common image formats, common audio and video formats, PDF, and plain text. Anything outside that set is served as an anonymous binary download instead, and the response carries the header that tells browsers not to guess the type from the content.
The reason is that these bytes come from a third party but are served from your own domain. If a provider could return a page of markup or a scriptable vector image and have your browser treat it as active content, that content would run with your dashboard's origin. Forcing it into a download closes that path completely, at the cost of an occasional extra click on an unusual file type.
The filename is rebuilt rather than trusted. It is taken from the tail of the provider URL, stripped down to letters, digits, dashes, underscores and dots, capped at sixty characters, and given an extension derived from the content type if it does not already have one. A filename ends up in a response header, and headers are not a place to pass through text someone else chose.
Looking at media does not require switching replies on
There is one small decision here worth knowing about, because it will save someone an afternoon.
The credentials used to fetch Twilio media are looked up without checking whether outbound messaging is enabled for that channel. That is on purpose. Viewing something the customer already sent you is an inbound act, and it would be a strange product that made you turn on the ability to send replies before it would let you look at the message you were replying to.
So if your channel is configured for receiving only, inbound photos still open. If the credentials are missing entirely, the request is still attempted and the provider's own refusal is reported rather than being hidden behind a local error, which is the more useful failure of the two.
Reading the failure when it does fail
Failures here are specific rather than generic, and the distinction tells you where to look.
A refusal about the host means the stored link points somewhere that is not on the allow list, which is a configuration or provider question rather than a network one. A missing attachment means the comment has no stored media at that position. A provider status failure carries the number the provider actually returned, so an authentication problem at the provider looks different from an expired link. A size failure means the media was larger than the ceiling, not that it was rejected as unsafe.
That last distinction matters when a customer says they sent something and you cannot see it. Provider media links also expire on the provider's own schedule, so an old ticket may hold a link that no longer resolves. In that case the right move is to ask the customer to send it again rather than to investigate your own system, because nothing on your side has broken.
How MyLiveChat fits
The short version is that inbound media on your connected messaging channels, arriving from WhatsApp, SMS and Messenger, is stored as the provider's own link, fetched server side against an exact host allow list with one permitted redirect, and streamed to an authenticated agent as bytes that can never be treated as active content on your domain.
For you that mostly means it simply works and you never see a provider URL. For anyone reviewing how customer data moves through your helpdesk, it means the answer to where those photos are stored is that they are not: they stay with the provider, and they are fetched on demand for one signed-in agent at a time.
If you are documenting attachment handling more broadly, live chat file uploads and attachment safety covers the other direction of travel, and how your channel message counts are counted explains what a media-only message does to your volume figures.