mapier docs

Checking reachability

Asking whether an address can receive iMessages before you send to it.

An address that is not on iMessage does not bounce. You send to it, the API returns 202, the command settles, and nothing arrives — because "succeeded" means iMessage accepted the send, not that it reached anyone. Nothing in the send path tells you the number was Android-only or the Apple ID never existed.

POST /v1/handle-check is how you find out first. It asks Apple's directory whether an address is reachable, and answers in the HTTP response — no command, no queue, nothing on the stream.

Asking

The address goes in the body, not the URL. That is deliberate: reachability queries are personal data, and a path or query string ends up in access logs everywhere along the way.

curl $MAPIER_BASE_URL/v1/handle-check \
  -H "Authorization: Bearer $MAPIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "address": "+14155550100" }'

A reachable address comes back like this:

200 OK
{ "address": "+14155550100", "available": true, "idStatus": 1 }

Address format is strict

The same grammar applies here as everywhere else in the API: E.164 phone numbers, or lowercase, NFC-normalised email addresses. There is no normalising step that will forgive a human-formatted number for you.

AddressResult
+14155550100Accepted
person@example.comAccepted
4155550100400 invalid_address — no leading +
(415) 555-0100400 invalid_address — not E.164
Person@Example.com400 invalid_address — must be lowercase
not a handle400 invalid_address

Normalise once, at the edge of your own system, and store the canonical form. Doing it at each call site is how one path ends up sending Person@Example.com.

Branch on available, never on idStatus

available is the boolean you make decisions with. idStatus is Apple's raw status integer, passed through unchanged. It has been observed as 0 and 1, but only its sign is contractual — a future value could be any positive integer for a reachable address. Code that tests idStatus === 1 will eventually be wrong; code that tests available will not.

const { available } = await res.json();
if (!available) return; // not on iMessage

When the answer is neither

The check runs against a live Mac, so it can be unavailable in ways a pure API cannot. Those come back as 503 with a reason:

503 Service Unavailable
{ "error": "handle_check_unavailable", "reason": "read_in_flight" }

Every reason describes the service rather than the address — except one.

reasonMeaningRetry?
read_in_flightAnother check is already running on that MacYes, shortly
connector_offlineThe Mac is not connectedYes, with backoff
deadline_exceededApple did not answer in timeYes
session_closedThe session ended mid-readYes
response_rejectedThe answer failed validationYes
read_failedThe read failed on the hostYes
capability_unavailableThe Mac does not support handle checksNot until the host changes
not_configuredHandle checking is not provisioned for this accountNot until it is provisioned — contact Mapier
invalid_targetApple definitively refused the addressNo — this is the answer

invalid_target is the only one that carries information about the address. It is not a fault in the service; it is an answer, and the answer is that the address will not work. Treat it exactly as available: false. Every other reason means you learned nothing, so do not cache it and do not conclude the address is bad.

Check serially, and cache

Reads are single-flight per Mac: one check runs at a time, and a second concurrent request gets read_in_flight rather than queueing. Firing off twenty checks in parallel to validate an import will mostly produce 503s.

Two habits follow from that:

  • Check one address at a time. A simple sequential loop with a short retry on read_in_flight beats any concurrency here.
  • Cache the result. Whether a number is on iMessage changes rarely — on the order of someone switching phones — so a result cached for hours or days is still accurate, and it keeps you clear of the 30-requests-per-minute limit on this endpoint. See Rate limits.

Checking before sending

Putting it together: a helper that returns a three-valued answer, because "I could not find out" is genuinely different from "no".

type Reachability = "yes" | "no" | "unknown";

async function checkAddress(address: string): Promise<Reachability> {
  const res = await fetch(`${BASE}/v1/handle-check`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAPIER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ address }),
  });

  if (res.status === 200) {
    const { available } = await res.json();
    return available ? "yes" : "no";
  }

  if (res.status === 503) {
    const { reason } = await res.json();
    // The only 503 that is an answer rather than an outage.
    return reason === "invalid_target" ? "no" : "unknown";
  }

  throw new Error(`handle-check failed: ${res.status}`);
}

Then send only on a definite yes, and decide deliberately what unknown should do — sending anyway is often the right call for a low-stakes message, and the wrong one for an onboarding flow that assumes the message landed.

async function sendIfReachable(address: string, text: string) {
  const reachable = await checkAddress(address);
  if (reachable === "no") return { sent: false, reason: "not_on_imessage" };
  if (reachable === "unknown") return { sent: false, reason: "check_unavailable" };

  await fetch(`${BASE}/v1/commands/message.send`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MAPIER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      target: { kind: "recipient", recipientExternalId: address },
      payload: { text },
    }),
  });

  return { sent: true };
}

Note that the send above uses a recipient target, which is plain text only and is not retry-safe — a repeated call sends a second message. See Addressing a conversation for why, and for the pattern of switching to a conversation target once the thread exists.

A passing check is not a delivery guarantee. It says Apple believes the address can receive iMessages right now. There are no delivery receipts in this API, so a succeeded settlement still means "iMessage accepted the send", not "it appeared on their phone".

On this page