How to Detect iMessage vs SMS Before Sending
Not every phone number supports iMessage. Android users, deactivated Apple IDs, and numbers that have been ported away from Apple devices all fall back to SMS. If your application treats iMessage and SMS differently — for routing, pricing, or user experience — you need a way to check before you send.
senderZ exposes a capabilities endpoint that tells you whether a given phone number can receive iMessage. The result comes back in under 200 milliseconds and is cached for 24 hours. You can use it to show a blue-bubble or green-bubble indicator in your UI, pre-sort a contact list before a campaign, or decide which message template to use based on the channel.
This guide covers how the detection works, how to call the endpoint, how to use it in batch workflows, and how to handle edge cases like recently ported numbers.
Why Detection Matters
When you send a message through senderZ with channel: "auto", the platform handles routing automatically. If the number supports iMessage, the message goes via iMessage. If not, it falls back to SMS. You do not need to detect capabilities for basic sending — auto-routing handles it.
But there are scenarios where knowing the channel in advance changes your application logic:
UI hints. A messaging interface can show a blue bubble icon next to iMessage-capable contacts and a green icon next to SMS-only contacts. Users understand the difference immediately.
Template selection. iMessage supports rich text, longer messages, and Tapback reactions. SMS has a 160-character segment limit and no read receipts. Choosing the right template based on the channel produces a better experience.
Batch pre-sorting. Before sending a campaign to 1,000 contacts, you can split the list into iMessage and SMS groups. This lets you track deliverability metrics per channel and adjust content for each group.
Cost planning. If your pricing model distinguishes between iMessage and SMS (or if you pass through carrier costs to customers), pre-detection helps forecast costs before sending.
Compliance messaging. Some compliance flows require different opt-in language depending on the channel. Knowing the channel ahead of time lets you present the right consent form.
The Capabilities Endpoint
The endpoint is straightforward:
GET /v1/capabilities/:number
Pass a phone number in E.164 format (e.g., +15551234567) and get back the channel capabilities for that number.
Using curl
curl -X GET https://api.senderz.com/v1/capabilities/+15551234567 \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"data": {
"number": "+15551234567",
"imessage": true,
"sms": true,
"checked_at": "2026-04-17T14:30:00Z",
"cached": false
}
}
The imessage field is the one you care about. When it is true, this number can receive iMessage. When false, the number will receive SMS instead.
The cached field tells you whether this result came from the 24-hour cache or from a fresh probe. On the first request for a number, cached will be false. Subsequent requests within 24 hours return cached: true with no additional latency.
The checked_at timestamp tells you when the probe was last performed. If you need a fresh check after a cache hit, you can pass ?force=true to bypass the cache.
Using TypeScript
const response = await fetch(
"https://api.senderz.com/v1/capabilities/+15551234567",
{
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
}
);
const { data } = await response.json();
if (data.imessage) {
console.log("This number supports iMessage");
} else {
console.log("This number is SMS-only");
}
Using the senderZ SDK
import { SenderZ } from "@senderz/sdk";
const client = new SenderZ({ apiKey: "YOUR_API_KEY" });
const capabilities = await client.capabilities.check("+15551234567");
console.log(capabilities.imessage); // true or false
For SDK installation and setup, see the quickstart guide.
How Detection Works Under the Hood
When you call the capabilities endpoint, senderZ checks its internal cache first. If the number has been probed within the last 24 hours, you get an instant response with no backend work.
If the cache is cold, senderZ performs a real-time probe against the Apple iMessage registration system using dedicated Apple hardware. The probe determines whether the number is registered with Apple’s push notification service for iMessage. This is the same lookup that happens on any Apple device when it decides whether to send a blue bubble or a green bubble.
The probe takes between 50 and 200 milliseconds. The result is cached for 24 hours because iMessage registration status rarely changes — a number that supports iMessage at 9 AM almost certainly still supports it at 5 PM. The 24-hour TTL balances freshness against infrastructure load.
For more details on how senderZ routes messages across channels, see the send message documentation.
Batch Detection
If you need to check hundreds or thousands of numbers at once — before a campaign, during a contact import, or as part of a nightly sync — you can call the capabilities endpoint in parallel. senderZ supports up to 50 concurrent requests per API key.
Here is a batch detection pattern in TypeScript:
import { SenderZ } from "@senderz/sdk";
const client = new SenderZ({ apiKey: "YOUR_API_KEY" });
const numbers = ["+15551234567", "+15559876543", "+15550001111"];
const results = await Promise.all(
numbers.map(async (number) => {
const capabilities = await client.capabilities.check(number);
return {
number,
imessage: capabilities.imessage,
};
})
);
const imessageNumbers = results.filter((r) => r.imessage);
const smsNumbers = results.filter((r) => !r.imessage);
console.log(`iMessage: ${imessageNumbers.length}`);
console.log(`SMS only: ${smsNumbers.length}`);
For lists larger than a few hundred numbers, add a concurrency limiter (like p-limit) to stay within the 50-concurrent-request limit:
import pLimit from "p-limit";
const limit = pLimit(50);
const results = await Promise.all(
numbers.map((number) =>
limit(async () => {
const capabilities = await client.capabilities.check(number);
return { number, imessage: capabilities.imessage };
})
)
);
This pattern scales to tens of thousands of numbers. Each request that hits the cache resolves in under 10 milliseconds, so cached lookups are essentially free.
Use Case: Show Channel Indicator in UI
A common pattern is to display a channel badge next to each contact in a messaging interface. Here is a React component that does this:
function ChannelBadge({ number }: { number: string }) {
const [channel, setChannel] = useState<"imessage" | "sms" | "loading">("loading");
useEffect(() => {
fetch(`https://api.senderz.com/v1/capabilities/${number}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => res.json())
.then(({ data }) => setChannel(data.imessage ? "imessage" : "sms"));
}, [number]);
if (channel === "loading") return <span>Checking...</span>;
return (
<span className={channel === "imessage" ? "text-blue-500" : "text-green-500"}>
{channel === "imessage" ? "iMessage" : "SMS"}
</span>
);
}
Because the cache persists for 24 hours, calling this on every render is safe and fast after the first probe.
Use Case: Pre-Campaign Channel Split
Before sending a marketing campaign, split your contact list by channel to tailor content:
const contacts = await client.contacts.list();
const checks = await Promise.all(
contacts.map(async (contact) => {
const cap = await client.capabilities.check(contact.phone_number);
return { ...contact, imessage: cap.imessage };
})
);
// Send longer, richer messages to iMessage contacts
const imessageContacts = checks.filter((c) => c.imessage);
// Send concise SMS-optimized messages to the rest
const smsContacts = checks.filter((c) => !c.imessage);
This approach improves deliverability and engagement because each channel gets content optimized for its strengths. For campaign setup details, see the solutions page.
Edge Cases
Recently ported numbers. When someone switches from iPhone to Android, their number may remain registered with iMessage for up to 24 hours (sometimes longer if they did not deregister). During this window, a capabilities check will return imessage: true but the message may not deliver via iMessage. senderZ handles this gracefully — if iMessage delivery fails, the message automatically falls back to SMS within seconds.
Numbers with iMessage disabled. Some iPhone users disable iMessage in Settings. Their number will show imessage: false even though they use an iPhone. Messages to these numbers go via SMS, which is the correct behavior.
Landlines and VoIP numbers. These always return imessage: false and sms: true. Standard SMS delivery applies.
Force refresh. If you suspect a cached result is stale (for example, you know a contact just switched devices), pass ?force=true to the endpoint to bypass the cache and run a fresh probe.
Rate Limits
The capabilities endpoint shares the same rate limits as your plan tier. Each check counts as one API call. Cached responses still count toward your rate limit but return faster.
For current rate limits and plan details, see the pricing page.
Integration with Claude Code
If you use the senderZ MCP server with Claude Code, you can check capabilities using natural language:
"Check if +15551234567 has iMessage"
"Which of these numbers support iMessage: +15551234567, +15559876543"
Claude Code calls the capabilities endpoint through the MCP server and reports the results back to you. No code required.
Frequently Asked Questions
How accurate is the iMessage detection?
The detection relies on Apple’s own iMessage registration system — the same mechanism every iPhone uses to decide between blue and green bubbles. Accuracy is over 99% for numbers that have been actively used within the past 30 days. The only inaccuracy window is when someone recently deregistered from iMessage but the status has not propagated yet, which typically resolves within a few hours.
Does the check send a message to the number?
No. The capabilities endpoint performs a registration lookup only. No message is sent, no notification is triggered, and the recipient has no way of knowing the check happened. It is a read-only, non-intrusive probe.
How long does the cache last?
Results are cached for 24 hours from the time of the probe. After 24 hours, the next request triggers a fresh probe. You can bypass the cache at any time by passing ?force=true to the endpoint.
Can I check international numbers?
Yes. The capabilities endpoint works with any phone number in E.164 format, including international numbers. iMessage is available in every country where Apple sells iPhones, so international detection works the same way as domestic detection.
Is there a bulk endpoint for checking many numbers at once?
There is no dedicated bulk endpoint, but you can call the single-number endpoint in parallel with up to 50 concurrent requests. For most batch workflows — contact imports, pre-campaign checks, nightly syncs — this concurrency is sufficient to process thousands of numbers in seconds.
Ready to start detecting iMessage capabilities? Get your API key at senderZ.com, install the SDK, and call the capabilities endpoint on your first number in under a minute.
For the full iMessage detection documentation, visit the iMessage detection docs. To learn how auto-routing uses capabilities data when sending messages, see the send message reference.