Skip to content
zelnum.com
Carrier lookup · Async batch · 2,000-row minimum

Carrier Lookup API: Enrich a Phone List, 2,000 Rows at a Time

Send a phone list, get back which network each number sits on. One asynchronous job, results as a file you can join straight back to your own records.

Product slug
hlr-lookup
Mode
async task
Batch floor
2,000 rows

Need a product explanation for ported carrier routing? See Number Portability Check.

POST /api/v1/detect
curl -X POST "https://api.zelnum.com/api/v1/detect" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "hlr-lookup",
    "country": "US",
    "phones": ["+14155550198", "+442071838750"]
  }'
The job is accepted, not finished
modeasync
task_idqH5rZ7Lp3Wm9
summary.total10000
summary.cost_deducted4.0

HTTP 202. Nothing has been looked up yet, and the balance is already reserved. The carrier data arrives through the download.

Job lifecycle

What happens after you submit the list

Four moves, and the first one is the only one that costs you anything up front. The carrier answers arrive at the end, not in the submit response.

1. Submit

POST the array, or upload a file. The full amount is reserved at this moment, before a single number is queried.

2. Receive a task

HTTP 202 with a task_id. This is a receipt, not a result. Treat the submit response as the end of the submission, not the end of the work.

3. Poll the status

The status call returns a progress percentage and the counters. Rows resolve in batches, so partial answers are real answers for the rows that landed.

4. Download

Ask the result endpoint for a download URL, then copy the file. The link expires, and an expired link is gone rather than merely slow.

The envelope fields, the ten counters and every error code are documented once, on the HLR lookup API page, because both pages run the same task machinery. This page is about the carrier job itself: what a row contains, what it costs at real list sizes, and which design choices the batch format forces on you.
Batch shape

The 2,000-row floor decides how you build the client

This product will not accept a short list. If you have 40 numbers to enrich, this endpoint is the wrong tool and the single lookup is the right one. If you have 400,000, the floor is irrelevant and the rest of this section is what matters.

Counter What it counts What you do with it
submitted Rows you sent Compare against your own input count first. A mismatch means the parser, not the lookup, is the problem.
prefiltered Rows dropped before any query Duplicate and malformed input lands here. A high number means your source list is dirty, which is worth fixing upstream.
deduped Repeats removed Free reduction in cost if you deduplicate locally first, and a signal when a CRM export repeats the same number across records.
total Rows actually accepted This is the number the reservation is computed from, so it is the one to reconcile your invoice against.
The practical consequence: one job per country. A mixed list is rejected on the country field rather than quietly resolved against the wrong numbering plan, which is the failure mode that produces plausible but wrong carriers. Split the file by country code, submit each part with its own country, and keep the country in your join key.
Output

What each row can tell you, and what it cannot

The download holds one row per accepted number. Read the header row rather than assuming a column order: field names are stable per product, positions are not a contract.

What the lookup answers

Which network the number belongs to, the line type when one is reported, and the outcome of the lookup itself. That is the entire product: a routing decision per row, derived from numbering data rather than from anything you told us.

What it does not answer

Whether the person behind the number is reachable, whether they consented, whether the handset is online, or who they are. A carrier result is not an identity check and it is not a deliverability guarantee.

Field coverage differs per product and per market. Do not build a pipeline that assumes a porting flag, a roaming state or an MCC/MNC pair is present unless your account's product returns it, because a missing column fails later and quieter than a wrong value does. Probe one small batch first and read the header.
Freshness

Carrier data goes stale because numbers move

Portability is the reason this page cannot promise permanence. A number keeps its digits when the subscriber changes network, so a carrier result describes the day you ran it, not the rest of the list's life.

Numbers move without changing

A ported number is indistinguishable from a stable one by looking at it. Only a fresh query settles where it currently routes.

Age the decision, not the file

Store the lookup timestamp beside the carrier. A routing rule that reads a two-year-old carrier without knowing its age is guessing with a straight face.

Re-run the segment that matters

Refreshing the whole database on a schedule is expensive. Refreshing the numbers you are about to message is not.

Two pages go deeper on this: why carrier data goes stale for the mechanics of portability, and number portability check for the non-developer version.

Workflows

Three routing rules the output actually supports

Carrier enrichment is only worth the money if a rule reads it. These three are the ones that survive contact with real traffic.

Suppress by line type

VoIP and landline numbers rarely belong in an SMS campaign. Drop them before the send rather than paying for delivery attempts that cannot succeed. The VoIP checker explains the line-type signals.

Choose a path by network

Carrier is the field that decides which route a message takes, and route quality differs. Group the list by carrier and send through the path that performs for that network.

Refresh before a high-value send

Transactional traffic deserves a fresh lookup; a quarterly newsletter does not. Spending $4 per 10,000 rows on the segment that matters is a rounding error against one failed send.

For the longer version, see carrier lookup for SMS routing and carrier lookup use cases.

Cost

What a carrier job costs at real list sizes

Prices live in the pricing catalog, and this page reads them from there rather than restating a number that can drift. HLR Lookup costs $4 per 10,000 rows with a minimum batch of 2,000.

List size Reserved at submit What the shape tells you
2,000 $0.80 The floor. Paying the minimum is the honest answer for a small test.
10,000 $4.00 One unit price. Compare vendor quotes on this row before negotiating anything larger.
50,000 $20.00 The size where deduplicating locally starts paying for itself.
250,000 $100.00 Above the 50 MB file ceiling if the numbers are long, so expect to split into two jobs.
1,000,000 $400.00 Half the platform maximum. Run it per country and reconcile each job separately.

The reservation is not the invoice.

Bulk jobs reserve against the accepted row count up front, then refund what never got checked. The status call exposes cost_deducted and cost_refunded, so the net charge is a subtraction you can audit instead of a number you have to trust.

A quote endpoint exists, with a caveat.

There is a bulk quote call for pre-flighting a large list without reserving balance, and its own documentation still describes the request body as incomplete. Do not wire it into a cost model until the shape is settled; estimate from the table above instead.

Integration

Array or file: pick by size, not by preference

Both forms create the same kind of task and return the same envelope. The difference is where the list has to live before it is submitted.

JSON array

Good up to a few tens of thousands of rows, when you already hold the numbers in memory. Numbers go in international format and the country is a separate field.

POST https://api.zelnum.com/api/v1/detect
X-API-Key: YOUR_API_KEY

{
  "product": "hlr-lookup",
  "country": "US",
  "phones": ["+14155550198", "+14155550132"]
}

File upload

Multipart upload for anything large: a TXT or CSV with one number per line, capped at 50 MB. This is the form that survives a million-row export without a memory spike.

curl -X POST "https://api.zelnum.com/api/v1/detect/file" \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "product=hlr-lookup" \
  -F "country=US" \
  -F "[email protected]"

The rate limit shapes the client as much as the list does.

Submissions are limited to 10 per minute per API key, reads to 60 per minute. A poll loop that checks twice a second spends the entire read budget on one job and gets a 429 with a Retry-After header for its trouble. Poll once a second at most, and read Retry-After instead of inventing a backoff.

Debugging

The five errors this job actually produces

Most of these share HTTP 422, which is why the status code alone will send you looking in the wrong place. Branch on code. The message field is written for humans and is not stable.

code HTTP What it means
product_not_found 422 The slug is not in the catalog. Read GET /v1/products instead of copying a slug out of a tutorial, including this one.
product_mode_not_supported 422 A realtime product was sent to the bulk endpoint, or the reverse. Bulk endpoints take slugs; realtime endpoints take numeric ids.
invalid_country 422 Missing, or not an ISO 3166-1 alpha-2 code. Phone products require it; this product is a phone product.
too_few_numbers 422 Below the product floor, which is 2,000 here. Add rows or use the single lookup.
result_expired 404 The file aged out. Note that this returns 404, not 410, so an existence check that treats 404 as "wrong task id" will misread it.
Two more worth knowing before they surprise you: idempotency_conflict at 409 when the same key arrives with a different body, and detection_capacity_unavailable at 503 when the queue is full. The second one is temporary, and retrying it later is correct rather than abusive.
One trap

The cost field changes type between two calls

This is the single most common integration bug we see on the bulk flow, and it does not throw.

At submit

summary.cost_deducted is a JSON number. Arithmetic works, and so does a strict comparison.

When you poll

cost_deducted and cost_refunded are strings, because they are money and money should not round-trip through a float. Parse them as decimals before subtracting.

In JavaScript, "4.0" - "1.2" is fine and "4.0" + "1.2" is not, so a reconciliation job that adds two refunds together produces "1.21.2" and writes it to your billing table without complaint. Convert once at the boundary, then keep decimals throughout.
Alternative

Compare with Twilio Lookup API

Twilio is the safer choice if you already run messaging there, because carrier data and sending sit behind one account. This is the better fit when the job is bulk enrichment of a list you already own, priced per row and returned as a file.

Read the Twilio alternative
Category zelnum.com Twilio Lookup API
Unit of work A list, 2,000 rows minimum One number per request
Bulk handling Native task: submit, poll, download a file Your loop, your concurrency, your retries
Result shape Downloadable file, one row per number JSON per call
Fits when The list exists already and needs enriching The lookup sits inline in a messaging flow
Libraries

SDKs: one is published, the rest is plain HTTP

Being precise about this is cheaper than a support ticket. There is one official client, and everything else is an HTTP call you can make from any language in about twenty lines.

Python, published

Version 0.1.1, installable from PyPI as zelnum. Use it if your enrichment runs as an offline job, which is the usual shape.

pip install zelnum

Everything else, today

No published packages for Node, Ruby or PHP. The API is a single POST plus a poll, so the honest recommendation is curl or your own HTTP client rather than waiting for a client library.

curl -X POST https://api.zelnum.com/api/v1/detect ...
FAQ

Carrier Lookup API FAQ

The questions that come up before the first production job.

How many numbers can one job take?
A minimum of 2,000 and a maximum of 2,000,000 per job. File uploads are capped at 50 MB, with one number per line in a TXT or CSV file.
Does the list need a country code?
Yes, on both counts. Each number goes in international format, and the request carries a separate country field with an ISO 3166-1 alpha-2 code. A list that spans countries becomes one job per country.
What happens to numbers that cannot be resolved?
They are counted as unknown rather than reported as invalid, which is the distinction that keeps you from deleting good rows. Rows that never get checked are refunded, and the status call reports cost_deducted and cost_refunded so you can see the net.
Can I resubmit the same list safely?
Send an Idempotency-Key header. A repeat submission returns the original task with idempotent_replay set, instead of creating a second job and reserving the amount twice. This is the cheapest protection against a retry after a timeout.
How long do results stay downloadable?
The status call carries result_expires_at. After that the download returns result_expired with HTTP 404, which is easy to misread as a wrong task id. Copy the file into your own storage the moment the task completes rather than fetching it on demand weeks later.
Are there webhooks for bulk jobs?
No webhook delivery is defined in the API contract. Completion is detected by polling, at most once a second for a single key, which the 60 requests per minute read limit allows with room to spare.
AI answer engines

Request an AI summary of ZelNum

Open ZelNum in your preferred AI assistant to review its bulk WhatsApp, Telegram, phone validation, carrier lookup, social account, crypto exchange, CSV upload, and API workflows.

The prompt points assistants to ZelNum.com, live pricing, product pages, and API documentation.