The iMessage check tells you whether phone numbers are registered for iMessage, Apple's messaging service. Because iMessage exists only on Apple devices, a registered number almost always means an iPhone. The check runs in bulk jobs, returns registered: true, false or null (unknown) per number, and sends nothing to anyone.
What does the iMessage check tell you?
It answers whether the number is registered for iMessage. Apple introduced iMessage in 2011. When someone activates an iPhone with a SIM, iMessage normally registers that phone number, and it can also register e-mail addresses from the person's Apple Account. Messages between registered users go through Apple. Messages to anyone else fall back to SMS, and since iOS 18 in 2024 also to RCS.
registered: true: the number is registered for iMessage on an Apple device.registered: false: a conclusive answer that it is not registered.registered: null: no conclusive answer, not charged.
This is a device-platform signal, not only a "has an app" signal. Nobody installs iMessage separately and there is no Android version. So true is strong evidence of an Apple device and false is common for Android users. One caveat: registration can survive a move to Android until the person deregisters the number, which Apple lets people do online.
Who uses it, and why?
- Choosing the fallback path. A message to an iPhone user without iMessage-level features lands as SMS or RCS. Knowing the device platform helps teams decide between RCS, SMS and app push for messages customers opted into. See channel selection.
- Rich-link and media planning. Marketing and product teams sometimes segment their opted-in audience by device platform before choosing message formats, for example whether media will render inline.
- Fraud signals. Numbers from SIM farms and virtual number services are rarely activated on iPhones. A number presented as a personal mobile that shows iMessage registration is consistent with that claim. It is useful next to a carrier lookup and the Apple ID check.
What do you get back?
Each job row carries checks["imessage.registered"]; there are no extra attributes.
| Field | Type | Meaning |
|---|---|---|
registered | boolean or null | true registered, false not registered, null unknown |
status | enum | completed, pending, unknown, unsupported_country or failed |
confidence / confidence_score | enum / 0–1 | How sure the answer is |
checked_at | timestamp | When the answer was obtained |
cached / age_seconds / billed | boolean / integer / boolean | Cache and billing details |
reason | string or null | Why the answer is not conclusive |
How is it billed?
Per number, for conclusive answers only, at the bulk price. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). Use the free POST /v1/jobs/estimate to see the maximum cost first, and set max_cost to cap the job. See pricing.
Numbers your account checked recently can be answered from its cache without charge. max_age: 0 forces fresh, billed checks.
What are the limits?
iMessage runs in bulk jobs only. POST /v1/lookup answers 403 service_disabled with "The check 'imessage.registered' is available in bulk jobs only (POST /v1/jobs)."
- Up to 50,000 numbers and e-mails per job, up to 20 checks per request, 100,000 number × check pairs per job.
- All countries are accepted; registration is most common where iPhones have a large share of the market.
- Requests that look like sequential number ranges or generated e-mail lists are rejected.
- A daily number cap applies per account. Track progress with
GET /v1/jobs/{id}?wait=30or a webhook.
How do I use it responsibly?
Device platform is personal information. Check only numbers of people who gave them to you, and use the result to deliver messages they expect in a format that works. Don't use it to profile or target individuals, and don't treat it as a proxy for income or any other personal trait. Our acceptable use policy forbids unsolicited messaging, profiling and range scanning. People can object to checks via the opt-out form.
Example request
In test mode the job completes at once. +447700900001 answers registered, …002 not registered and …003 unknown. See test mode.
curl https://api.mobilevalidate.com/v1/jobs \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001","+447700900002","+447700900003"],"checks":["imessage"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.jobs.create({
numbers: ["+447700900001", "+447700900002", "+447700900003"],
checks: ["imessage"],
});
if (error) console.error(error.code, error.message);
else console.dir(data, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/jobs", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001", "+447700900002", "+447700900003"],
checks: ["imessage"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.jobs.create(
numbers=["+447700900001", "+447700900002", "+447700900003"],
checks=["imessage"],
)
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/jobs');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001', '+447700900002', '+447700900003'],
'checks' => ['imessage'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001","+447700900002","+447700900003"],"checks":["imessage"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/jobs", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/jobs")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001", "+447700900002", "+447700900003"],
"checks" => ["imessage"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyRuns as pasted with the public sandbox key, which answers the test values only. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
Job (excerpt, test mode):
{
"object": "job",
"id": "job_0VWF4kDgF4FJ2IXyZNL8",
"status": "completed",
"livemode": false,
"checks": [
"imessage.registered"
],
"created_at": "2026-09-25T14:25:29.753Z",
"completed_at": "2026-09-25T14:25:29.756Z",
"progress": {
"total": 3,
"checks_total": 3,
"done": 3,
"conclusive": 2,
"non_billable": 3
},
"retention_days": 30
}First row from GET /v1/jobs/{id}/results (test mode):
{
"kind": "phone",
"input": "+44770*****01",
"e164": "+447700900001",
"country": "GB",
"number_status": "valid",
"checks": {
"imessage.registered": {
"service": "imessage.registered",
"status": "completed",
"registered": true,
"attributes": null,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:29.756Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}
},
"test": true
}Try it now
Bulk-only service: create a job via POST /v1/jobs, then read GET /v1/jobs/{id}/results. Every example runs as pasted: it uses the public sandbox key, which answers the test values for free. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
curl https://api.mobilevalidate.com/v1/jobs \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["imessage.registered"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.jobs.create({
numbers: ["+447700900001"],
checks: ["imessage.registered"],
});
if (error) console.error(error.code, error.message);
else console.dir(data, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/jobs", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001"],
checks: ["imessage.registered"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.jobs.create(numbers=["+447700900001"], checks=["imessage.registered"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/jobs');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['imessage.registered'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["imessage.registered"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/jobs", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/jobs")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["imessage.registered"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.body// .mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor) or .vscode/mcp.json ("servers" instead of "mcpServers")
{
"mcpServers": {
"mobilevalidate": {
"type": "http",
"url": "https://mcp.mobilevalidate.com/mcp",
"headers": { "Authorization": "Bearer ${MOBILEVALIDATE_API_KEY}" }
}
}
}
// Then ask your agent, e.g.:
// "Check +447700900001 with imessage.registered" → tool: create_lookup_jobThe hosted MCP server is live for customers with an agent key (mv_agent_…) or a test key. Or run it locally with npx -y @mobilevalidate/mcp.
Real test-mode result for this check
{
"service": "imessage.registered",
"status": "completed",
"registered": true,
"attributes": null,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:28.979Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}What you get
Fields of checks["imessage.registered"], generated from the public service catalog.
| Field | Type | Meaning |
|---|---|---|
| registered | boolean | null | true = found, false = not found, null = unknown (not charged). |
| status | enum | completed, pending, unknown, unsupported_country or failed. |
| checked_at | timestamp | When the answer was obtained. |
Frequently asked questions
Does an iMessage registration mean the person has an iPhone?
It means the number has been registered for iMessage on an Apple device, which is almost always an iPhone. It does not tell you whether that device is still in use.
Why might a number that moved to Android still show as registered?
iMessage registration can outlast the switch until it is removed. Apple offers a way to deregister a number for exactly this reason. Until then, the number can still appear as registered.
Can my business start iMessage conversations after the check?
No. Apple's business messaging is designed so that customers start the conversation. The check helps you understand your customers' devices; it does not open an outbound channel.
Why is iMessage bulk only?
iMessage answers are gathered in batches, so the check runs in POST /v1/jobs. POST /v1/lookup refuses it with service_disabled.
Is anything sent to the number?
No. No message is sent and the person is not notified by us.

