The RCS check tells you whether phone numbers can currently receive RCS messages: the richer successor to SMS built into phone messaging apps. It runs in bulk jobs and answers registered: true, false or null (unknown) per number. When the handset platform is reported, it adds device_os (ios, android or unknown). Nothing is sent to the number.
What does the RCS check tell you?
It answers whether the number is RCS-capable right now. Unlike app-based messengers, RCS is not an account someone signs up for. It is a feature of the carrier network and the phone's default messaging app, standardised by the GSMA as the Universal Profile. On Android it runs in the messaging app; Apple added RCS to iOS 18 in 2024. It works only where the carrier supports it and the user has not turned it off.
registered: true: the number can receive RCS messages.attributes.device_osmay say whether the handset runs iOS or Android.registered: false: a conclusive answer that the number is not RCS-capable now. Messages to it would be delivered as SMS or MMS.registered: null: no conclusive answer; not charged.
Capability changes more often than messenger registrations do: a new phone, a disabled setting or a carrier change can flip it. Keep max_age short when you use the answer to route messages.
Who uses it, and why?
- RCS business messaging rollouts. Brands adopting RCS for verified, branded messages check which opted-in customers can receive them and send SMS to the rest. See SMS cost reduction.
- Channel selection. RCS can carry rich cards, suggested replies and read receipts for messages people asked for, such as delivery updates or appointment confirmations. See channel selection.
- Device platform awareness.
device_ostells teams whether a customer is on iOS or Android without asking, which helps choose between app push, iMessage-friendly formats and RCS. - Deliverability signal. RCS capability shows a live number on a smartphone with a supporting carrier, which complements a carrier lookup.
What do you get back?
Each job row carries checks["rcs.registered"].
| Field | Type | Meaning |
|---|---|---|
registered | boolean or null | true RCS-capable, false not capable, null unknown |
attributes.device_os | enum ios, android, unknown | Handset platform reported with the capability, when available |
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 |
Downloads include an rcs.registered.device_os column.
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). The free POST /v1/jobs/estimate shows the maximum cost before you start. See pricing.
Answers your account received recently can be served from its cache at no charge. Because capability changes, consider a shorter max_age than for messenger checks. max_age: 0 forces fresh, billed checks.
What are the limits?
RCS runs in bulk jobs only. POST /v1/lookup refuses it with 403 service_disabled ("The check 'rcs.registered' is available in bulk jobs only (POST /v1/jobs).").
- Up to 50,000 numbers and e-mails per job, 20 checks per request, 100,000 number × check pairs per job.
- All countries are accepted, but
trueanswers only occur where carriers support RCS. - Requests that look like sequential number ranges or generated e-mail lists are rejected.
- A daily number cap applies per account. Follow the job with
GET /v1/jobs/{id}?wait=30or a webhook.
How do I use it responsibly?
Use RCS capability to deliver messages people agreed to receive in a format their phone supports. Carrier and industry rules for RCS business messaging require verified senders and consent, and our acceptable use policy forbids unsolicited bulk messaging and range scanning. Don't use device_os to infer anything about a person beyond the handset platform. People can object to checks via the opt-out form.
Example request
In test mode +447700900001 answers capable, …002 not capable and …003 unknown. Test answers do not include device_os. 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":["rcs"]}'// 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: ["rcs"],
});
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: ["rcs"],
}),
});
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=["rcs"],
)
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' => ['rcs'],
]),
]);
$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":["rcs"]}`)
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" => ["rcs"]
})
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_0VWF4kFfnr2scbsj4bNU",
"status": "completed",
"livemode": false,
"checks": [
"rcs.registered"
],
"created_at": "2026-09-25T14:25:29.875Z",
"completed_at": "2026-09-25T14:25:29.879Z",
"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": {
"rcs.registered": {
"service": "rcs.registered",
"status": "completed",
"registered": true,
"attributes": null,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:29.879Z",
"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":["rcs.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: ["rcs.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: ["rcs.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=["rcs.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' => ['rcs.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":["rcs.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" => ["rcs.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 rcs.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": "rcs.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["rcs.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. |
| attributes.device_os | enum | Handset platform reported with the RCS capability.ios · android · unknown |
Frequently asked questions
What is RCS?
RCS (Rich Communication Services) is the carrier messaging standard that adds typing indicators, read receipts, high-resolution media and branded business messages to the phone's built-in messaging app. It is defined by the GSMA Universal Profile.
Do iPhones support RCS?
Yes, since iOS 18 in 2024, where the user's carrier supports it. That is why the result can report device_os ios as well as android.
Why can RCS capability change from one day to the next?
Capability depends on the carrier, the handset, the messaging app and whether the user has RCS chats turned on. A SIM moved to another phone, or a setting changed, can switch it on or off.
Does RCS capability mean I can send RCS business messages?
Not by itself. Business messaging over RCS needs a verified sender and carrier approval, and it may only be used for messages the recipient agreed to receive.
Why is device_os missing from some answers?
device_os is included only when it is reported with the capability answer. When it is not, the key is absent or unknown. Test-mode answers do not include it.

