Messaging Apps & Chat Platforms

A chat message gives the reader about one second to decide

Email trained people to hesitate. Chat trained them not to. A link that arrives inside a thread they were already reading, from a name they already know, gets tapped before the preview has finished loading. This page is about where a platform can put a hostname check — at send, at unfurl or at render — and how to do it inside a latency budget that leaves nothing to spare.

Sub-50msAPI response, so a check fits inside a send acknowledgement
390,000+Live phishing hostnames, each verified as currently resolving
100Domains per batch request, for message history and channel audits
0 lookupsLeave the device when the list is downloaded and matched locally
Send-path checking
Unfurl and preview
On-device matching
Group and channel spread
SMS and RCS shortlinks
Interstitial warnings
Home / Use Cases / Messaging Apps
The pipeline

Five moments where a hostname could be checked, and only three are any good

The question is not whether to check. It is which stage of the message lifecycle the check belongs to.

One message, one link, five decision points
ComposeClient sees the raw URL as it is typed or pasted
SendServer pipeline can extract hosts before fan-out
UnfurlThe preview fetch already resolves the destination
RenderRecipient's client can match locally, even under encryption
TapLast chance — an interstitial, and only if you own the browser

Compose is too early: the user has not committed, and warning them about their own paste is noise. Tap is too late for most platforms, because the operating system has already handed the URL to a browser you do not control. The three in the middle are where real products put the check.

Stage one

At send: block, do not warn

Checking at send has one property nothing else can match — it stops the message rather than warning about it. Catch a hostile host before fan-out and nobody in a five-thousand-member channel ever sees the link, with no window between delivery and moderation for it to do its work. That is the right place for a platform that controls the server pipeline and does not promise end-to-end encryption for the conversation in question.

Stage two

At unfurl: the work is already happening

The service generating the link preview has to resolve the URL, follow redirects and fetch metadata anyway, so adding a hostname check costs one call on a path that is already asynchronous and already tolerant of latency. It also lands the verdict exactly where attention is: the preview card is what people look at, so replacing it with a warning beats appending a notice underneath the message.

Stage three

At render: the only option under encryption

If the server genuinely cannot read the message, the check has to happen where the plaintext exists, which is the recipient's device. That pushes the design toward a downloaded list rather than a per-message query, and it changes the engineering problem from "call an API" to "keep a compact local dataset current". More work — and for a platform whose selling proposition is privacy, the only honest option.

In practice

Mature deployments do more than one

Send-path checking for unencrypted channels and bot traffic, unfurl checking as a second pass that catches whatever the send path could not resolve, and a retrospective sweep over recent messages when the daily list changes. The three are not redundant: they cover different failure modes, and the third exists because a link that was clean on Tuesday can be verified as hostile on Wednesday.

Encryption constraints

What you can check when you have deliberately blinded yourself

End-to-end encryption is a promise about what the server cannot see. Link checking that quietly breaks it is worse than none.

The engineering conversation about phishing in an encrypted messenger tends to go wrong in the first five minutes, because someone proposes server-side URL scanning and someone else points out that this requires the server to read messages. Both are right, and the resolution is not a compromise on encryption — it is moving the check to the client and accepting the constraints that come with it. Those constraints are real, and they are also narrower than people expect.

The reference data lives on the device

Downloading a list of hostnames and matching locally is the whole design: the message never leaves the device, the hostname never leaves the device, and no per-message lookup exists to be logged, subpoenaed or correlated. The full database ships as CSV or JSON, downloads are unlimited on a subscription, and a client can pull it on its normal update schedule.

The privacy claim is unusually strong

With a downloaded list, checking a link reveals nothing about the user to anyone — not to you, not to the data provider, not to a network observer. With per-message API lookups, someone somewhere accumulates a record of which hostnames your users' messages contained. For a product chosen specifically because of what the server cannot see, no privacy policy repairs that difference.

Where server-side checking is fine

If your infrastructure can already read message content, sending a hostname to a checking service adds no new disclosure about content you did not already hold, so per-query checking is a reasonable trade. Even then, high-volume platforms usually move to the local list anyway — at pipeline scale, an in-memory set-membership test beats a network round trip on every axis, including the ones finance cares about.

Local match, no lookup leaves the device Daily refresh on the client's normal update cycle Changelog-driven deltas instead of full reloads Server-side checking only where plaintext already exists Key held server-side, never shipped in a client build
The key never ships in the app. Your API key is simply the username you chose at registration, and it belongs on a server you control. A mobile client should fetch the list from your own distribution endpoint, which authenticates to the feed on the server side — anything embedded in a client build is extractable in minutes.
Deltas, not full reloads. A daily changelog of domains added and removed ships with the feed, so a mobile client can apply a small diff rather than re-downloading the whole database every day. On a metered connection that difference is the whole feature.
How it spreads

Six shapes that only exist because messaging is social

Email phishing is a broadcast. Chat phishing is a chain reaction, and that is why a delayed response costs so much more here.

The trusted account

One member's credentials are harvested elsewhere, and the attacker then posts from inside. Every recipient sees a colleague's name, a real avatar and a thread they were already following. No header analysis helps, because nothing about the message is forged.

The group invite

An invite link drops a user into a channel populated with plausible-looking members and a pinned message containing the payload. The social proof is manufactured, cheap, and considerably more persuasive than any email pretext.

Bot and integration abuse

Bots post at machine speed and are often exempted from the rate limits and moderation heuristics applied to humans. A compromised or malicious integration can seed hundreds of channels before anyone reads one of them.

Smishing shortlinks

SMS and RCS carry the delivery-notice and bank-alert lures, almost always behind a shortener because of the character limit. Resolve the redirect chain first and check the final hostname, not the shortener's.

Links inside attachments

A shared document, spreadsheet or slide deck carries the URL inside the file rather than in the message body. A scanner that only reads message text sees nothing at all, which is exactly why the technique persists.

The delayed activation

A benign host is shared today and turned hostile in three days, after any send-time check has already passed. Only a retrospective sweep against the updated daily list closes this one.

The last of those deserves emphasis because most implementations forget it. A check at send time is a statement about that moment only, and attackers know it — staging the infrastructure through register, park, share, then activate is a well-worn way to defeat point-in-time scanning across every medium. The counter is cheap: keep an index of hostnames shared in the last few weeks, and each morning join it against the newly-added entries in the daily changelog, then replace the preview, disable the link or append a warning depending on how aggressive your product wants to be.

That job is small. You are not re-checking every message ever sent, only a bounded recent window against a delta of a few hundred additions. It runs in seconds, uses almost no credits, and converts a static defence into one that improves after the fact.
The latency budget

Why sub-50ms is the number that decides the architecture

Users notice a message that takes longer than about a tenth of a second to appear sent. Everything you add has to fit inside what is left.

<50msTypical API response, before any network distance you add
10 / secRequests per API key — the reason bulk paths use the feed
1 callUp to 100 domains, so a link-heavy message is still one round trip
04:30 UTCDaily build, with a changelog of what was added and removed

Work the budget backwards. A user tolerates roughly a hundred milliseconds between tapping send and the message appearing as delivered before the interface starts to feel sticky, and out of that you have already spent time on transport, authentication, persistence and fan-out. Whatever a security check consumes comes out of the remainder — and if it does not fit, the check gets moved off the critical path, which is the correct engineering decision and also the exact moment the control stops preventing anything.

Batching keeps the synchronous version viable

Up to a hundred domains go in a single POST at one lookup each, with the response carrying checked, phishing_found and credits_used. Deduplicating hostnames before the call helps more than it sounds, because a message pasting the same host five times is one lookup rather than five.

Above a threshold, stop calling anything

Ten requests per second per key is generous for a checking service and immediately inadequate for a pipeline handling millions of messages a minute. That gap is not a limitation to route around; it is a signal about which product to use. Download the full database, hold it beside the message handler, and every check becomes a set-membership test measured in microseconds.

The verdict shape has no tuning surface

There are exactly two confidence values in a response — 0.98 when a match is confirmed and 0.0 when there is none — which removes any per-surface threshold and any ambiguous middle band to argue over. Everything else is equally unambiguous: is_phishing answers yes or no, category never varies from phishing/malware, dns_status reports resolves, and last_checked gives you a day you can show a user.

Synchronous at send Async at unfurl In-memory local match Nightly retrospective sweep Changelog deltas
Interstitial design

The warning screen is a product surface, not an error message

A badly written warning trains users to dismiss warnings, and every security feature you ship afterwards inherits that damage.

Most in-app phishing warnings fail for the same reason browser certificate warnings failed for a decade: they are vague, they are frightening, and they offer a "continue anyway" button that becomes muscle memory within a week. The purpose of the screen is not to express severity — it is to give the user one specific fact they did not have, phrased so that the right choice is the obvious one rather than merely the cautious one.

Be concrete

Say what was found, and how

"This address has been verified as an active credential-harvesting site" tells the user something checkable. "This link may be unsafe" tells them nothing and invites them to make their own assessment, which is precisely the assessment the tool exists to spare them. If you hold a last_checked date, show it — recency is what makes a claim like this credible to a sceptical user.

Be careful with the exit

An override should cost a deliberate act

A confirmed match against a verified list is a much stronger signal than a heuristic, and it justifies a harder stop than most warnings can. If the product needs an override for legitimate reasons — a researcher, a journalist, a moderator inspecting a report — make it deliberate rather than a same-size button beside the safe one, and log it when it is used.

Tell the sender too

A posting account is usually a victim

In a group context, notify the sender as well as the recipients. An account distributing hostile links is far more often compromised than malicious, and the person who owns it needs to know before they lose considerably more than a channel. That notification is also the cheapest account-takeover signal your safety team will ever get.

Do not repeat yourself

Warn once per host, not once per message

When the same hostile host is posted forty times across a channel, forty interstitials is a denial-of-service on your users' attention. Collapse them into one thread-level notice and one moderation event, and let the abuse tooling handle the rest quietly in the background.

LINK BLOCKED

This address is a verified phishing site

The destination behind this link was confirmed as an active credential-harvesting page designed to look like a sign-in screen. Nothing about your message or your account was shared to determine this — the address was matched against a list already stored on this device.

Back to conversation Report this sender
Say what was not shared. If the match happened locally, a single sentence saying so is the most valuable line on the screen for a privacy-conscious user base — and it is a claim that is architecturally true rather than a policy promise.
Honest limits

What a known-bad list does not do for a chat platform

Stated plainly, because a platform team will discover it in week two and would rather have been told in week one.

This is a known-bad lookup and nothing more. When a hostname comes back clean, the only thing you have learned is that it has not been seen and verified — a much weaker statement than "safe", and one that will never become stronger. Infrastructure stood up forty minutes ago and fired into a single chat thread has had no opportunity to be observed by anybody, so it is simply not in the file, and that is a property of verification itself rather than a gap somebody forgot to close. Any product claiming otherwise is describing a heuristic and calling it a fact.

Gap one

Shorteners hide the destination

Checking a shortener's own hostname tells you about the shortener and nothing about where it goes. You have to follow the chain first, which is work your unfurl service is probably already doing and your send path probably is not. On SMS and RCS surfaces, where shorteners are effectively mandatory because of message length, skipping this step makes the whole check decorative.

Gap two

File-borne links are invisible to text scanning

A URL inside a shared document is not in the message body, so anything reading only message text sees nothing. That is exactly why the technique persists long after message scanning ships. Extraction has to happen in your upload pipeline — or, on an encrypted platform, client-side alongside the local match.

What it is good at

Volume, which is most of the problem

The commodity kits, the reused templates, the infrastructure rotating through a campaign that also hit four hundred other services this week — that is the traffic actually flooding a group chat. Removing it means your abuse team and your users spend attention on the small residue that is genuinely novel, alongside the approaches on our URL shorteners, social media and identity protection pages.

What it costs

Two shapes, matching the two architectures

Targeted checking — reported messages, new-account first posts, bot traffic, moderation queues — is credit-shaped work, and credits run through PayPal on a twelve-month expiry: 10,000 lookups for $59, 25,000 for $99, 100,000 for $249, 250,000 for $499, and larger tiers beyond. Once every message is in scope, the Daily Threat Feed at $499/month is the right shape, because downloads are unlimited and the per-check cost falls to zero.

What the annual feed adds. Historical archive access, priority support, custom format options, a named account manager and 100,000 API credits. Payment by bank transfer opens above $4,000, and anything needing SFTP or S3 delivery, STIX/TAXII output or a non-daily update rhythm is priced by conversation rather than from a table.
Resolve before you check. Checking a shortener's own hostname produces a confident, useless answer. Follow the redirect chain to the final host, cap the hop count, and check what you land on.
From the platform team

What engineering, trust and safety, and legal ask first

Eight questions, including the one about whether this is compatible with the privacy claim on your marketing site.

Can we do this without breaking end-to-end encryption?

Yes, and it is the intended pattern rather than a workaround: the client downloads the phishing domain list on its normal update cycle and matches locally against hostnames it already holds in plaintext, because it is the endpoint — the message is never decrypted anywhere else, no hostname is transmitted for checking, and the server learns nothing it did not already know. What you do need to get right is distribution, because your own backend must authenticate to the feed and republish the list to clients rather than embedding a key in an app binary, and that indirection also lets you ship a compact derived structure instead of the raw file if device storage is a constraint.

Where exactly should the check sit in a chat pipeline?

If you can read message content, put the primary check in the send path before fan-out, because blocking beats warning and it protects everyone in a channel at once; then add a second pass in the link-preview service, which is already resolving the URL and following redirects and is the natural place to catch whatever the send path could not resolve; then run a nightly retrospective job over recently shared hostnames against the daily changelog. If you cannot read message content, the send and unfurl stages are unavailable and everything moves to the client at render time. Do not make tap time your only check — on most platforms the operating system has already handed the URL to a browser you do not control.

Won't a synchronous check make sending feel slow?

At single-message volumes, no — responses come back in under 50 milliseconds and a whole message's hostnames go in one batched request rather than one per link, so even a link-heavy message is a single round trip. Two things keep it honest in production: deduplicate hostnames before calling, and set an aggressive timeout with a defined fallback, which for most consumer products means failing open and letting the unfurl pass and the nightly sweep pick up the slack. Once you are past a few thousand messages per second the answer changes entirely, and the correct move is to stop calling anything and hold the list in memory beside the handler.

How do we handle shortened links?

Resolve first, then check: a shortener's hostname is not the destination, and checking it tells you only that the shortener is not itself a phishing domain, which is almost always true and almost never useful. Follow the redirect chain server-side with a hop cap and a timeout, check the final hostname, and check any intermediate hosts too if the chain crosses several parked domains — which is one reason the unfurl service is such a good integration point, since it already does that resolution work to build the preview card. On SMS and RCS surfaces, where shorteners are effectively mandatory because of message length, this step is not optional at all.

What about links inside shared files?

A hostname check cannot help until something extracts the hostname, and that extraction is on your side of the boundary: if your platform already processes uploads for previews, thumbnails or text indexing, that pipeline is where URLs should be pulled out of documents and batched through the same check as message text. If it does not, this gap stays open and you should say so internally rather than assume message scanning covers it, because the technique specifically exists to defeat scanners that read only message bodies. On an encrypted platform the extraction has to happen client-side alongside the local match, which is more work but architecturally identical.

Someone reports a link after it was delivered. What is the workflow?

Check the hostname immediately, because a confirmed match turns a judgement call into a fact and lets an automated action fire without a human in the loop. A verified result also lets you act on the whole blast radius rather than the single reported message — find every occurrence of that hostname in the recent window, disable the links, and notify the accounts that posted them, which are usually compromised rather than hostile. Keeping a bounded index of shared hostnames for a few weeks is what makes that possible, and it is the same index the nightly changelog sweep already uses.

Does using this contradict our privacy marketing?

Only if you deploy it as per-message cloud lookups on an encrypted product, which is the deployment to avoid for exactly that reason. With the downloaded feed, no query about any user's message ever leaves the device or your infrastructure, and the only outbound traffic is your own scheduled download of a threat list that reveals nothing about anybody. A researcher who decompiles your client and watches its network traffic will find that this holds, which is a far stronger position than a paragraph in a policy document — so write it into your transparency documentation in plain words: the app downloads a list of known phishing addresses and compares links to it locally.

How fresh is the data, and what happens to domains that get taken down?

The database is rebuilt every 24 hours, the daily build lands at 04:30 UTC, and inclusion requires a verified active A record confirmed through rotating proxy infrastructure. Anything that stops resolving is dropped at the next rebuild instead of lingering, so the file stays a working picture of what is live — which matters a great deal for a client-side deployment where every kilobyte is shipped to a phone. Because a changelog of additions and removals ships alongside, a client can apply a delta rather than pulling the whole file again, and because /stats is open — no key, no credits, no auth — your build pipeline can assert on database size and last-update date at no cost at all.

Put the check where the message actually passes

Confirm the database size through the open statistics endpoint, wire one batched call into the unfurl service, and measure what it costs you in milliseconds before committing to anything. If every message is in scope, the downloaded feed is the shape that keeps latency flat and keeps user lookups off the network entirely.