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 MarkdownPOST /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?
{
"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"}
}| Field | Rules |
|---|---|
numbers, emails | At least one; 1–100 identifiers in total. Numbers in any format; national formats need default_country |
checks | Service codes or aliases, up to 20 (default ["whatsapp"]). Phone checks run on numbers, e-mail checks on emails |
default_country | ISO 3166-1 alpha-2, e.g. GB |
max_age | Seconds; accept a cached answer up to this age. 0 forces a fresh, billed check |
wait | 0–30 seconds to hold the request open (default 10) |
max_cost | Refuse with 402 cost_limit_exceeded if the maximum possible cost is higher |
webhook_endpoint_id | A verified endpoint that receives lookup.completed |
metadata | Up 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):
{
"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.
kindisphoneoremail; treat a missingkindasphone. - Phone rows have
e164,countryandnumber_status(valid,invalid_number,duplicate,suppressed). E-mail rows haveemailandemail_statusinstead (see e-mail checks). checksholds 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.registeredisfalseonly for a conclusive negative.nullmeans not conclusive. Data services (carrier, spam) put their answer inattributesand setregistered: truewhen data was found.whatsappis a compatibility copy of the WhatsApp result in the original v1 shape, withbusinessadded whenwhatsapp.businesswas requested. It appears only when awhatsapp.*check was requested. New code should readchecks.summarycounts rows for the first requested service of each row's kind.summary.by_servicegives the counts per service. For data services, "registered" means data found.billingshows 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:
"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 withcached: true, the answer's age inage_seconds, andbilled: false.max_age: 0forces 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 exceedsmax_cost, you get402 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 withIdempotent-Replayed: true. The same key with a different body gets409 idempotency_key_reused. A retry while the first request is still running gets409 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.

