API guides
How to use the API, beside the reference that describes every field. Start at the top if you are new; the sections stand alone otherwise.
Your first request
No key, no signup, nothing to configure. Every endpoint answers an anonymous caller on the smaller of two allowances.
curl https://longestmatch.com/api/v2/lookup/8.8.8.8
That returns where the address appears to be, whose network announces it, who the block is registered to, whether it carries VPN, proxy, Tor or hosting signals, what BGP is doing with it, and two separate scores with the evidence behind each.
To look up whichever address you are calling from, use /api/v2/lookup/self.
It is the same answer for a different subject, and it is served
private, no-store so no shared cache can hand your address to somebody
else.
The other half of the API is about networks. Once you
know an address sits on AS15169, /api/v2/directory/asn/15169 tells you what
that network's whole address space holds. Most integrations end up calling both.
Getting a key
Create an account, confirm the address, and issue a key from your account page. It is free and it takes a minute. The key is shown once, because only its hash is stored: one that is lost is replaced rather than recovered.
Send it in the X-API-Key header:
curl -H "X-API-Key: $LM_KEY" https://longestmatch.com/api/v2/directory/asn/15169
A key is never required to try the API: every route answers anonymously on the smaller
allowance, and only POST /lookup/batch is gated, on Pro and above. What an
account mainly changes is volume.
The allowance belongs to the account, not to the key. Every key you hold draws on one pool, and so does this site while you are signed in, so a key per environment costs nothing and tells you which environment is spending what.
Needing more than the free allowance offers? The plans list what each one carries. Past the largest, write to [email protected] with a sentence on what you are building and the volume you expect.
Looking up many addresses at once
One request, up to a hundred addresses:
curl -X POST https://longestmatch.com/api/v2/lookup/batch \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"ips": ["8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"]}'
You get one entry per address, in the order you sent them, each with
either a result or an error. Match them up on
query, which echoes the string you sent rather than the normalised form
of it.
{
"requested": 3, "answered": 2, "charged": 2,
"results": [
{ "query": "8.8.8.8", "result": { ... }, "error": null },
{ "query": "nonsense", "result": null, "error": "Not a public IP address." }
]
}
Three things worth knowing before you build against it.
One bad address does not fail the batch. It comes back as one failed entry among the answers, so a list read straight out of your logs does not have to be cleaned first.
You are charged one token per distinct valid address. Repeats are
looked up once and charged once, and still get an entry each. Rejected addresses cost
nothing. charged tells you what it came to, and
X-RateLimit-Cost carries the same number.
There is no enrich here. That parameter makes a live
registry query per address, and a hundred of those in one request is a minute spent
inside somebody else’s service. Use GET /lookup/{ip}?enrich=true for
the addresses that need it.
Batch is included on Pro and above. Every other route is on every plan, free included.
A plan without it gets a 403 naming the plans that have it, and a
403 costs nothing against your allowance.
Rate limits, and handling a 429
A limit per minute, and an allowance that refills continuously as you spend it. Run out and you wait minutes for enough to come back, never until midnight. Both belong to your account rather than to each key, so a key per environment costs you nothing: it tells your usage apart and it does not divide your allowance.
Every response carries where you stand, so you can watch it without waiting to be refused:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The most your allowance holds when full. |
X-RateLimit-Remaining | How much of your allowance is left now. |
X-RateLimit-Reset | Unix time by which it has refilled completely. You can call again well before this. |
X-RateLimit-Cost | What this request cost. Usually one; a search costs two, and a batch costs one per distinct address in it. |
A 429 adds Retry-After in seconds. Sleep for that long and
repeat the request. A refused request costs nothing against your allowance,
so a retry loop that honours Retry-After is safe to leave running.
/api/v2/meta/health and /api/v2/meta/ready are unmetered, so
a probe never competes with your real traffic.
Reading a breakdown without getting it wrong
This is the part worth ten minutes. Every directory resource returns tables of shares, and each one carries the four things you need to read it honestly.
"flags": {
"rows": [ { "label": "Datacenter", "share_v4": 0.55, "share": 0.55, "ranked_by": "v4" } ],
"shown": 3,
"distinct_total": 3,
"truncated": false,
"coverage": { "v4": 0.63, "v6": null },
"residual": { "v4": 0.37, "v6": null },
"overlaps": true
}
Quote distinct_total, never len(rows)
rows is a top-N. NordVPN's exit locations return eight rows against a real
count of 149. If you render "8 countries" from that array you have published a number
that is wrong by a factor of eighteen, and nothing in the response looks unusual.
truncated tells you when the two differ.
When you want all of them
Every breakdown has a full version, a hundred labels to a page:
GET /api/v2/directory/vpn/nordvpn/countries # 100 of 149
GET /api/v2/directory/vpn/nordvpn/countries?page=2 # the remaining 49
The breakdown name is the same one it has in the profile, hyphenated where it is two
words: countries, organisations, flags,
vpn-providers, networks, operators,
hosts. Which of them a scope publishes depends on the scope, and asking a
network for its operators gives a 404 rather than an empty page.
The profile stays a summary on purpose: the widest scope holds 17,343 networks. Check
distinct_total there first, since it tells you whether the full resource is
worth the requests. Page size is fixed at 100, and walking every network in the United
States costs 174 of them.
coverage is the denominator, and it is not 1
A table's shares are of its own kind, so the anonymizer table sums to
100% of the flagged space and not of the network. coverage says how much of
the whole scope carries any label of that kind at all. Read together: 55% of the flagged
space is datacenter, and flagged space is 63% of the network. Read apart, you will
conclude that 55% of the network is a datacenter.
residual closes the table
The part of the scope carrying no label of that kind. It is given, so you never have to compute a remainder from truncated rows.
overlaps means the rows may exceed 100%
Anonymizer categories overlap constantly: a range flagged as a datacenter is usually
flagged as a proxy too. When overlaps is true the labels can cover one
address several times, so the rows are not a partition and summing them means nothing.
Such a table is not broken, and share_of_covered_v4 is null on it.
IPv4 and IPv6 are never added
share_v4 counts addresses, share_v6 counts /64s, and there is
no unit that makes them comparable: one /32 allocation holds more /64s than the entire
IPv4 internet holds addresses, so any combined figure is decided by whichever family you
scaled up. share and ranked_by tell you which family the table
was ordered by, which is IPv4 wherever the scope has any.
null means the scope holds none of that family. It is a different claim
from holding zero of it.
Walking from an address to its network
Every row and every entity reference carries api_url beside
page_url, so you can follow a result without rebuilding URLs from display
labels. That matters more than it sounds: several spellings fold to one organisation, so a
slug guessed from a label lands on a 404. api_url is only ever emitted
where the target provably exists.
A typical chain:
GET /api/v2/lookup/8.8.8.8
-> analysis.network.asn = 15169
GET /api/v2/directory/asn/15169
-> operator.api_url = /api/v2/directory/isp/google-llc
-> breakdowns.countries.rows[0].api_url = /api/v2/directory/country/us
GET /api/v2/directory/isp/google-llc
-> members[].api_url, one per network the operator runs
If you only have a name, /api/v2/directory/search?q= resolves it. Its
total is a real count from the database, and when you narrow with
kind it still reports what matched in the categories it excluded, under
elsewhere, so a narrowed search never implies that something absent does not
exist.
Scores, evidence and warnings
Two scores, deliberately kept apart. scores.abuse is what
has been reported against the address space. scores.concealment is how much
the origin of traffic is hidden. Merging them is what makes commercial risk products flag
ordinary VPN users as fraudsters: somebody on a corporate VPN scores high on concealment
and nothing on abuse, and that distinction is usually the useful part.
Each score carries factors, the signals that produced it. Prefer the band or
the factors over the number, since the number moves whenever the evidence is weighed
differently and that is stated as outside the version contract.
Check the evidence grade before trusting a flag
anonymizer.evidence grades each flag as confirmed (the operator
publishes this address), listed (an independent list names the block) or
inferred (a wide block, an ASN, or a registry record). 96.3% of flagged IPv4 space is
covered only by wide inferred blocks, so a boolean alone would describe 3.7% of the data
with confidence it has not earned.
warning means the sources disagree
Where the evidence behind a field conflicts, that field carries a warning
in prose. A block registered in one country and used in another is ordinary for VPNs,
content networks and hosting, so a mismatch is usually meaningful rather than an error.
Prose is not part of the contract
analysis.text, every summary, every warning and
every insights[].text are written for a reader and get rewritten. Every
figure they mention is also a structured field. Read the field.
Versioning and what stays put
/api/v2/ is canonical. /api/v1/ and the unversioned paths
still resolve and still answer, so nothing pointed at them returns a 404. They are not
frozen: one response model serves every prefix, so they carry the same fields and the same
data v2 does. Every field that survives keeps its name and its declared type. One did not:
location.accuracy_radius_km is removed on every prefix, including v1, because
no remaining source publishes a confidence radius and a permanently null key invites a
caller to build on it. location.precision and location.block_prefix
replace it, in a unit the data supports. Otherwise what changes is how often a nullable
field is null.
Fields are added in minor versions and never renamed or removed without a major one, so ignoring a field you do not recognise is always safe. Two things sit outside that promise and both are stated in the reference: score values, which move with the evidence, and prose.
GET /api/v2/meta/version reports what a deployment answers as, every
response carries X-API-Version, and the full history is at
/docs/changelog.
What the data cannot tell you
Worth knowing before you build on it.
Registered holders are missing for the Americas
Only three of the five regional registries publish bulk whois. ARIN and LACNIC do not, so American holders are largely absent: US space accounts for roughly 40 million addresses here against the 1.6 billion it actually holds. Comparing a German organisation against an American one will mislead you unless you account for that.
Country means where space is used
Directory country figures come from geolocation, which is where addresses are used. The registration country is a separate question, and the two disagree for 4.9% of IPv4 space and for 61.2% of VPN space. A lookup reports both.
Geolocation is a block estimate, and two sources have to agree
Location describes a block and never a device, and this API is not for identifying a household, an individual or a street address.
country.evidence says how well two sources agree about the
country. confirmed means both name it, and it is the only state that carries
a city, a region or coordinates. contested means they name different
countries: the more reliable one is published, the city is withheld, and
country.alternative carries the other reading in full so you can decide for
yourself. single_source means only one source has an answer and nothing
corroborates it.
A city is withheld rather than guessed because a city belongs to the country it sits in. Publishing one source’s city under the other’s country would be a false answer rather than a vague one. This affects about a fifth of lookups, because the addresses people query most are public resolvers, CDN edges and cloud endpoints. Those are where the two sources disagree most.
Coordinates get a second check the country does not: the point has to fall inside the
named country’s own footprint. Agreement is about the country, so a block once
published GB with London beside coordinates in Madison,
Wisconsin, and was called confirmed. Where the point falls outside,
location.lat and location.lon are null and the country, region
and city stay. country.alternative gets the same check.
Reputation decays
Addresses get reassigned, so a listing from last year says little about who holds an
address today. reputation reports how many independent lists agree and how
recently they were confirmed, and it never renders a verdict.
Breakdown shares have their own trap and their own chapter: reading a breakdown covers what each denominator divides.
Per-dataset refresh times are not published. reputation.last_confirmed
still dates the one kind of answer where staleness changes the meaning, and
country.evidence tells you how well the geolocation sources agree.
Reverse DNS, and when to wait for it
reverse_dns carries the PTR hostname the block's operator publishes for an
address. When it is there it often names the operator more precisely than any dataset
does. It is also the only part of a lookup that leaves our network, and only about a
quarter of addresses have one at all.
So a lookup does not wait for it. The field is answered from a cache with a seven-day life, and an address we have not seen before is queued and resolved in the background, which means the answer is there on a later request. That took 79 ms off a response that spent 285 ms building an answer.
Reading an empty field
An empty reverse_dns.value with no warning means one thing:
this address has no PTR record. That is a real answer and it will not change by asking
again.
An empty value with a warning means we have not got one for you. The warning says which happened: the address is queued and has not been resolved yet, the resolver did not answer within half a second, or reverse DNS was too busy to look. In every one of those cases the address may well have a hostname.
curl 'https://longestmatch.com/api/v2/lookup/8.8.8.8'
# "reverse_dns": { "value": null, "warning": "Not looked up yet. ..." }
Asking us to wait
Add reverse_dns=true when you need the hostname in this response rather than
a later one. It resolves inline and costs up to half a second more latency.
curl 'https://longestmatch.com/api/v2/lookup/8.8.8.8?reverse_dns=true'
# "reverse_dns": { "value": "dns.google", "warning": null }
For bulk work the cheaper pattern is to leave it off, walk your addresses once to queue them, and walk them again later. The second pass reads from cache and costs nothing extra.