Real-time lookups

Check up to 100 phone numbers and e-mails in one request: multi-check, waiting and polling, the results shape, caching, max_cost and idempotency.

Last updated

View as Markdown

POST /v1/lookup checks 1–100 phone numbers and/or e-mail addresses against one or more services and waits up to 30 seconds for the answers. It returns 200 when every answer is ready. Otherwise it returns 202 with status: "pending" and a URL to poll. Use it for real-time decisions, such as a sign-up form or an OTP send. For lists, use bulk jobs.

What goes in the request?

JSON
{
  "numbers": ["+447700900001", "07700 900002"],
  "emails": ["[email protected]"],
  "checks": ["whatsapp", "telegram", "email"],
  "default_country": "GB",
  "max_age": 604800,
  "wait": 10,
  "max_cost": {"amount": "0.05", "currency": "USD"},
  "webhook_endpoint_id": "we_…",
  "metadata": {"crm_id": "C-1042"}
}
FieldRules
numbers, emailsAt least one; 1–100 identifiers in total. Numbers in any format; national formats need default_country
checksService codes or aliases, up to 20 (default ["whatsapp"]). Phone checks run on numbers, e-mail checks on emails
default_countryISO 3166-1 alpha-2, e.g. GB
max_ageSeconds; accept a cached answer up to this age. 0 forces a fresh, billed check
wait0–30 seconds to hold the request open (default 10)
max_costRefuse with 402 cost_limit_exceeded if the maximum possible cost is higher
webhook_endpoint_idA verified endpoint that receives lookup.completed
metadataUp to 20 string keys, values up to 500 characters; echoed back

Unknown fields are rejected (400 invalid_request), so typos don't pass silently. Every check must have identifiers of its type: "checks": ["email"] with only numbers is refused with param: "checks[0]". Numbers × applicable checks may not exceed 2,000 per lookup.

How do multi-check requests work?

Every identifier gets one result per requested service of its input type. Numbers get phone checks and e-mails get e-mail checks. If you send e-mails without any e-mail check (or numbers without a phone check), the request is refused with param: "checks". That way you never pay for a request that couldn't answer. whatsapp.registered and whatsapp.business together collapse into whatsapp.business, because it answers both questions.

Some services are bulk only. On this endpoint they return 403 service_disabled: "The check 'signal.registered' is available in bulk jobs only (POST /v1/jobs)." The services reference marks them. GET /v1/services returns modes for each service.

What does the response look like?

Real test-mode output for {"numbers": ["+447700900001", "+447700900002", "+447700900003"], "checks": ["whatsapp", "telegram", "carrier"]} (first item, then the totals):

JSON
{
  "object": "lookup",
  "id": "lkp_0VWF4BLkr4ltR4n4dawy",
  "status": "completed",
  "livemode": false,
  "created_at": "2026-09-25T14:23:15.712Z",
  "results": [
    {
      "kind": "phone",
      "input": "+447700900001",
      "e164": "+447700900001",
      "country": "GB",
      "number_status": "valid",
      "checks": {
        "whatsapp.registered": {"service": "whatsapp.registered", "status": "completed", "registered": true, "attributes": null, "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:23:15.717Z", "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null},
        "telegram.registered": {"service": "telegram.registered", "status": "completed", "registered": true, "attributes": null, "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:23:15.717Z", "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null},
        "network.carrier": {"service": "network.carrier", "status": "completed", "registered": true, "attributes": {"line_type": "mobile", "carrier": "Test Carrier", "country": "GB"}, "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:23:15.717Z", "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null}
      },
      "whatsapp": {"service": "whatsapp.registered", "status": "completed", "registered": true, "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:23:15.717Z", "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null},
      "test": true
    }
  ],
  "summary": {
    "total": 3, "registered": 1, "not_registered": 1, "unknown": 1, "pending": 0, "invalid": 0, "suppressed": 0,
    "by_service": {
      "whatsapp.registered": {"completed": 2, "registered": 1, "not_registered": 1, "unknown": 1, "pending": 0},
      "telegram.registered": {"completed": 2, "registered": 1, "not_registered": 1, "unknown": 1, "pending": 0},
      "network.carrier": {"completed": 1, "registered": 1, "not_registered": 0, "unknown": 2, "pending": 0}
    }
  },
  "billing": {"billed_units": 0, "cost": {"amount": "0", "currency": "USD"}, "balance_after": {"amount": "9.97907", "currency": "USD"}},
  "next": null,
  "request_id": "req_0VWF4BLivGQN76TzQcXl"
}

How do I read a result item?

  • Rows come in input order: numbers first, then e-mails. kind is phone or email; treat a missing kind as phone.
  • Phone rows have e164, country and number_status (valid, invalid_number, duplicate, suppressed). E-mail rows have email and email_status instead (see e-mail checks).
  • checks holds one result per service, keyed by code, in request order. It is missing for invalid, duplicate and suppressed rows, which are never checked or billed.
  • registered is false only for a conclusive negative. null means not conclusive. Data services (carrier, spam) put their answer in attributes and set registered: true when data was found.
  • whatsapp is a compatibility copy of the WhatsApp result in the original v1 shape, with business added when whatsapp.business was requested. It appears only when a whatsapp.* check was requested. New code should read checks.
  • summary counts rows for the first requested service of each row's kind. summary.by_service gives the counts per service. For data services, "registered" means data found.
  • billing shows the checks billed in this request, their cost and your balance afterwards.

Clients should ignore fields they don't recognise and accept new enum values.

How do waiting and polling work?

The request waits up to wait seconds (default 10, maximum 30). If some answers are still outstanding, you get 202 with a Location header and:

JSON
"status": "pending",
"next": {"poll_url": "/v1/lookups/lkp_0VWF4BNZB4tgTHFOXsUy", "poll_after_ms": 2000}

Poll GET /v1/lookups/{id}?wait=30. This is a long poll: it returns as soon as everything is done, or after 30 seconds. Pending checks carry status: "pending" and poll_after_ms. With wait: 0 the request returns straight away, which suits webhook-driven flows. Test number +447700900004 stays pending for about 5 seconds, so you can try this out. When you read a lookup later, identifiers in input are masked (for example +44770*****04).

How do caching, costs and retries work?

  • Cache. Answers are cached per account, number (or address) and service. A request within max_age (default: each service's freshness window) is answered from cache with cached: true, the answer's age in age_seconds, and billed: false. max_age: 0 forces a fresh check, which is billed and rate limited.
  • Reservation. Before checking, the API reserves the maximum possible cost of the request. Each check is then settled when its answer arrives. Inconclusive checks (unknown, unsupported country, timeout, invalid, duplicate) are released, and you're not charged for them. If the reservation doesn't fit your balance, you get 402 insufficient_balance. If it exceeds max_cost, you get 402 cost_limit_exceeded.
  • Idempotency. Send Idempotency-Key: <1–255 printable ASCII characters, no spaces> on any POST. The same key with the same body within 24 hours replays the stored response with Idempotent-Replayed: true. The same key with a different body gets 409 idempotency_key_reused. A retry while the first request is still running gets 409 idempotency_request_in_progress (retryable). Keys are scoped to your account.

Frequently asked questions

What happens if an answer takes longer than my wait time?

The API returns 202 with status pending and a next.poll_url. Poll GET /v1/lookups/{id}?wait=30, or register a webhook for lookup.completed.

Is a pending result billed?

Only once it becomes conclusive. The price is reserved when you send the request and settled per check; unknown, unsupported, invalid and duplicate results are released and free.

Can I send the same request twice safely?

Yes, with an Idempotency-Key header. The same key and body within 24 hours replays the first response (Idempotent-Replayed: true) instead of checking again.

Why is the whatsapp object missing from some results?

The top-level whatsapp object is a compatibility copy that appears only when a whatsapp.* check was requested. Always read checks for new code.