Check a number is live before an agent sends an OTP
A well-formed mobile number can still be a disposable VoIP line, a recently ported SIM, or an absent subscriber. A live HLR lookup tells an agent which, before it spends on an OTP.
An agent about to send an SMS one-time passcode has a valid, well-formed mobile
number — and that is not enough. The number can be a disposable VoIP line
spun up to farm signup bonuses, a recently ported SIM that signals a possible
swap, or an absent subscriber that cannot receive the message at all. A live
HLR lookup — GET /phone/resolve — tells the agent
which of these it is before it pays a delivery provider for an OTP that will be
abused or bounce.
The problem: structural validity says nothing about liveness
GET /phone/validate confirms a number is well-formed
and mobile-typed, offline and for free-relative-to-a-network-call. But fraud and
deliverability live on the network, not in the numbering plan. A throwaway VoIP
number passes every structural check; so does a number whose handset has been
off for a week. The facts an agent actually needs before trusting a number — is
it active, who operates it now after porting, is it a non-fixed VoIP line —
only exist in real-time carrier (HLR) data, which no static dataset can
reproduce.
The call: fraud and deliverability signals from one live lookup
GET /phone/resolve takes a number (and optional country), runs an HLR
lookup when the number is mobile-eligible, and folds the raw signals into a
verdict the agent can branch on:
| Field | What the agent learns |
|---|---|
active | Subscriber reachable at lookup time; null when the HLR can’t tell |
carrier | The operator that runs the number now (after porting) |
mnp | Whether the number was ported, and from which carrier |
roaming | Whether the subscriber is roaming abroad |
risk | Derived signals folded into a single level |
coverage | Whether the live core it paid for was actually delivered |
The risk block is the headline. It is derived deterministically from the HLR
signals, provider-agnostic:
"risk": {
"non_fixed_voip": false,
"recently_ported": true,
"absent_subscriber": false,
"level": "medium"
}
non_fixed_voip flags a disposable line unfit for a verification code;
recently_ported a possible SIM-swap or port-out; absent_subscriber a number
that cannot receive the OTP right now. level is high on any strong signal
(VoIP or absent), medium if only ported, otherwise low — a single field an
onboarding flow can threshold on.
It bills honestly, and only when the lookup is worth it
The endpoint will not charge an HLR lookup that cannot help, and it never dresses a failed lookup up as a paid answer:
- Structurally invalid → 200
valid: falsewith anissue, no HLR call (snapshot). The agent does not pay for a network lookup on garbage input. - Valid but non-mobile (a fixed line) → 200, structure only, network fields
null, no HLR call (snapshot). Onlymobile,fixed_line_or_mobileandvoipnumbers are HLR-eligible. - Mobile-like → HLR lookup: a cache hit is
cached(withage_secs), a miss islive(and gets cached), and an upstream failure is a typed5xx— the agent is not charged for an answer it never got.
Per the x402 golden rule, the agent pays for the answer to its question. An
invalid or non-mobile number is a successful answer → 200. The 5xx range is
reserved for an HLR provider that is unreachable, timed out, or unconfigured.
Read active and coverage together — don’t conflate them
A subtle but important honesty point: active and coverage.complete are
independent signals, and a payout/onboarding decision must read both.
active | coverage.complete | Meaning |
|---|---|---|
true | true | Subscriber reachable; full live core |
false | true | Subscriber genuinely unreachable; full live core |
null | true | Carrier known, presence simply not reported |
null | false | Degraded fallback — neither presence nor carrier |
A carrier-only verdict (active: null, complete: true) is a complete
answer the provider could not enrich with presence — not a failure. A partial
fallback (active: null, complete: false, with a reason) never delivered
the live core at all, though it is still billed at the full price. Treating
“presence unknown” as “unreachable” would wrongly reject good numbers; coverage
is the field that keeps that distinction honest.
Where it sits in the x402 loop
Resolution is the live check that gates an expensive or trust-bearing action. The agent’s loop:
- Discover the endpoint and call it; receive the
402. - Pay — sign the chosen rail and replay the request.
- Resolve — read
risk.level,active,carrier, andcoverage. - Branch — block or step-up on
risk.level: highor a non-fixed VoIP line; apply a hold on a known-absent subscriber; proceed on a clean, active mobile — and only then spend on the actual OTP send.
Each paid call follows the same x402 pattern as every Invoket endpoint. The
Quickstart walks the whole discover → 402 → pay → replay
cycle with runnable snippets. Price and accepted rails are not pinned in this
article — they are served live by the
catalog; see the
endpoint reference for the current figure.
Validate cheaply, resolve only the survivors
The two phone endpoints are a cheap-then-expensive funnel, and using them in order is what keeps cost proportional to value:
GET /phone/validate— offline, structural, milliseconds. Run it on every number to normalize and drop the malformed.GET /phone/resolve— network-backed HLR intelligence. Run it only on the valid, mobile-typed numbers, and only when liveness or fraud risk actually gates a decision.
For a list — an onboarding batch, an SMS campaign list to clean —
POST /phone/resolve/batch resolves many
numbers under one x402 settlement instead of one per row.
Used for what it is — a live fraud and deliverability verdict — /phone/resolve
lets an agent spend its OTP budget only on numbers that can actually receive the
code and are not obvious throwaways. For the full field reference, billing
pipeline and error codes, see the
GET /phone/resolve documentation; for how agents
discover and call Invoket endpoints, see For agents.