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:

FieldWhat the agent learns
activeSubscriber reachable at lookup time; null when the HLR can’t tell
carrierThe operator that runs the number now (after porting)
mnpWhether the number was ported, and from which carrier
roamingWhether the subscriber is roaming abroad
riskDerived signals folded into a single level
coverageWhether 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: false with an issue, 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). Only mobile, fixed_line_or_mobile and voip numbers are HLR-eligible.
  • Mobile-like → HLR lookup: a cache hit is cached (with age_secs), a miss is live (and gets cached), and an upstream failure is a typed 5xx — 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.

activecoverage.completeMeaning
truetrueSubscriber reachable; full live core
falsetrueSubscriber genuinely unreachable; full live core
nulltrueCarrier known, presence simply not reported
nullfalseDegraded 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:

  1. Discover the endpoint and call it; receive the 402.
  2. Pay — sign the chosen rail and replay the request.
  3. Resolve — read risk.level, active, carrier, and coverage.
  4. Branch — block or step-up on risk.level: high or 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/validateoffline, structural, milliseconds. Run it on every number to normalize and drop the malformed.
  • GET /phone/resolvenetwork-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.