One comma, 77,085 companies: how the GLEIF API reads a pasted legal name
The first row looks right, which is why nobody checks row two

Start with the number, because the number is the argument. On 2026-09-13 I asked the official GLEIF register for entities whose legal name is "Apple, Inc." — written exactly as it appears on the company's own filings, comma included:
$ curl -s "https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=Apple,%20Inc.&page[size]=1"
... "total": 77085 ...
first result: "Apple Inc."
77,085 matches, out of a register holding 3,429,265 LEIs. Drop the comma and the full stop, and:
$ curl -s "https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=Apple%20Inc&page[size]=1"
... "total": 24 ...
first result: "Apple Inc."
24 matches. A ratio of roughly 3,200 to one, from one punctuation mark. And look at the last line of each block: both searches put Apple Inc. at the top. That is the part that makes this expensive rather than merely annoying. If you eyeball the first row, the broken search looks like it worked perfectly.
Every figure below comes from requests I sent to api.gleif.org/api/v1 on 2026-09-13, one at a time with several seconds between them, ten requests in total. GLEIF publishes new data daily, so I have also included what the same queries returned two days earlier, because the drift turns out to matter.
The comma splits your string into independent terms
What the register is doing is straightforward once you see it: the comma is a separator, and each side of it becomes a term that can match on its own. "Apple, Inc." is therefore not a company name to this API — it is the request "anything called Apple, or anything called Inc." Since "Inc" appears in the legal name of a large fraction of every American company ever registered, the second term swamps the first.
There is no error and no warning. The response is HTTP 200 with a well-formed JSON:API envelope, a meta.pagination.total of 77,085, and a first page whose contents are entirely reasonable. If you are paging at 200 records per request, the honest version of this query costs you one request; the broken version wants 386, and every one of them returns real companies that have nothing to do with Apple.
What makes it likely rather than merely possible is that a comma in a company name is correct. "Apple, Inc.", "Alphabet, Inc.", "Cargill, Incorporated" — that is how these names are written in filings, in contracts, and in the CSV a colleague sends you. Any pipeline that takes a legal name from a document and puts it into this filter will hit this, and the person who built the pipeline will have tested it with a name that happens not to contain a comma.
The fix is one line, and the reason to write it is not elegance but the second-order problem: once you strip commas, a user who genuinely meant "Siemens or Bosch" gets nothing, and a silent empty result is its own trap. So in my client the commas are replaced with spaces, and when the search then matches nothing, the row that comes back says in words that the commas were sent as spaces. The user can see which of the two things happened.
Whole words only, and the seven companies that prove it
The next assumption to break is that the name filter does substring matching. It does not. It matches whole words. I tested with a deliberately truncated term:
$ curl -s "https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=insur&page[size]=10"
... "total": 7 ...
Seven. Not zero, and not the several hundred thousand insurance companies in the register. Here they are, all seven, with the country of the legal address:
| Legal name | Country |
|---|---|
| INSUR, a.s. v likvidácii | SK |
| INSUR PATRIMONIAL SOCIEDAD LIMITADA | ES |
| INSUR PROTECCION VEGETAL, SA | ES |
| INSUR PROMOCION INTEGRAL SOCIEDAD LIMITADA | ES |
| INSUR INVEST SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ | PL |
| FREYA INSUR BROKER DE ASIGURARE-REASIGURARE SRL | RO |
| TALCOTT RESOLUTION LIFE & ANNUITY INSUR CO SEPARATE ACCOUNT THREE | US |
Every one of them has INSUR as a standalone word. Not one of them is a company whose name contains "Insurance". That is the whole rule, demonstrated in seven rows: the term has to be a word, and "insur" is not the beginning of a word as far as this index is concerned.
And seven is worse than zero would have been. Zero rows makes you go and check your query. Seven plausible rows, with real LEIs and real addresses, makes you conclude that the register is thin in your sector and move on. I would rather the API had returned nothing.
Worth knowing: GLEIF also exposes a full-text filter, and it is tempting as a workaround. It is not one, because it matches the address too. A full-text search for gibraltar returned 2,416 entities on 2026-09-11 — overwhelmingly companies registered in Gibraltar rather than companies called Gibraltar. Swapping one wrong answer for a different wrong answer is not progress.
A date range that stops at midnight on the last day
The same class of problem shows up in date filtering, and here the miss is close to half. Asking for entities first registered across a two-day window:
filter[registration.initialRegistrationDate]=2026-09-07..2026-09-08
-> HTTP 200, total 1,291
filter[registration.initialRegistrationDate]=2026-09-07..2026-09-08T23:59:59Z
-> HTTP 200, total 2,480
The plain a..b form returns 1,291 of 2,480 entities, which is 52.1 per cent. The upper bound is being read as an instant — midnight at the start of 8 September — rather than as a day, so everything registered after the first minute of the last day is outside the range. Append an explicit end-of-day timestamp and the count comes right.
This one is particularly good at hiding, because a half-open range is a completely normal API design and the result is never empty. Anyone building a daily job that fetches "companies registered yesterday and today" gets a number that looks like a number. The only way I found it was by summing the per-day counts and noticing they did not match the range count. If you take one habit away from this article, take that one: when a range query is available, also run it as its parts and check the parts add up.
A code it does not recognise is not rejected
GLEIF classifies legal form with ISO 20275 ELF codes — four-character strings like 2HBR for a German GmbH. The register also stores a free-text legal form for entities whose code was never filed. Those two facts combine badly:
| Request | HTTP | Total |
|---|---|---|
filter[entity.legalForm]=GMBH | 200 | 433 |
filter[entity.legalForm]=2HBR | 200 | 145,609 |
GMBH is not an ELF code. Instead of saying so, the filter matches the 433 entities whose legal form someone typed by hand as the literal string GMBH. Ask the intuitive question, get 0.3 per cent of the answer, with a 200 and no complaint. The first row of the GMBH result was "HEKA Herzog GmbH" — again, a first row that looks exactly right.
The same shape appears with country codes, where the failure is total rather than partial:
filter[entity.legalAddress.country]=ZZ
-> HTTP 200, total 0, data: []
ZZ is not a country. The API does not say that. It returns the identical response it would return for a real country that happens to have no registered entities. "You gave me nonsense" and "the answer is none" are the same bytes, which means a typo in a country column propagates through a pipeline as a legitimate empty result. The only defence is to validate against GLEIF's own list of 250 codes before the request leaves — the list is published, so there is no excuse for guessing.
The same queries, two days apart
Here is something I did not expect to be worth a section. I had figures for most of these queries from 2026-09-10 and 2026-09-11. Re-running them on 2026-09-13 gave:
| Query | 2026-09-10/11 | 2026-09-13 | Change |
|---|---|---|---|
| whole register | 3,426,628 | 3,429,265 | +2,637 |
Apple, Inc. | 77,060 | 77,085 | +25 |
Apple Inc | 24 | 24 | 0 |
2HBR (German GmbH) | 145,526 | 145,609 | +83 |
GMBH (free text) | 434 | 433 | −1 |
| 2026-09-07..2026-09-08 | 1,289 | 1,291 | +2 |
| … with end-of-day | 2,455 | 2,480 | +25 |
Two things follow from this. First, the free-text GMBH count went down by one, which is a reminder that a company register is not append-only; records get corrected and retired, and a count you cached last week can be larger than today's truth.
Second, and more usefully: a window of past registrations is not stable either. The 7–8 September window gained 25 entities between the 11th and the 13th, days after it closed. Registrations are backdated as they are processed. So "I already fetched that day" is not a safe assumption, and a daily incremental job that never revisits yesterday will be permanently short by whatever arrived late. None of the ratios in this article changed, which is why I quote the ratios and show the queries rather than asking anyone to trust a snapshot.
Ownership only answers from the top of the group
One last asymmetry, measured on 2026-09-11 rather than today. GLEIF's "owned by" filter returns the entities whose ultimate parent is the LEI you give it. Hand it Siemens AG and you get 573 entities. Hand it Siemens Healthineers AG — itself a Siemens subsidiary, with subsidiaries of its own — and you get 0, even though 87 entities report it as their direct parent.
This is not a bug; it is the filter doing what its name says. But "0" is the answer nobody reads carefully, and a mid-level holding company is exactly the kind of LEI somebody types in. Direct children come from a separate endpoint, and that endpoint silently ignores every other filter you attach to it — adding a country restriction to Siemens AG's direct-children list still returned all 461. A filter that is accepted and ignored is the same silence as a comma that means OR, arriving from a different direction, so the honest thing is to refuse to combine them rather than to let a caller believe the restriction was applied.
Checks that now run before the request leaves
Nothing here needs a clever workaround. All of it needs the request inspected before it is sent, which is a habit rather than a technique:
- Punctuation in a user-supplied string is an operator until proven otherwise. Commas, quotation marks and full stops all meant something to this API that the person typing them did not intend.
- Validate enumerated values against the publisher's own list. Country codes, legal form codes and status values are all published by GLEIF. A value not on the list should never reach the network.
- Never let an empty result stand alone. "No match", "your value was not recognised", "the filter was ignored" and "we did not reach the server" all look like zero rows and need four different fixes.
- Check a range against the sum of its parts. That single arithmetic check is what exposed the midnight boundary, and it would have exposed the comma too.
These are wired into the tool I maintain for this register — GLEIF LEI Scraper sends commas as spaces and says so in the result, validates country and ELF codes before the request goes out, extends date ranges to the end of the last day, checks each LEI's ISO 17442 check digits so a typo is reported as a typo instead of vanishing from a bulk lookup, and re-requests parent data on every page because GLEIF's own next-page link drops it. It also computes whether a renewal is overdue, because on 2026-09-10 there were 716 LEIs whose renewal date had passed while their registration status still read ISSUED.
Published by GRAMSHIFT. All counts above were read from live responses of api.gleif.org/api/v1: nine queries on 2026-09-13, plus three figures explicitly dated 2026-09-10 or 2026-09-11 and labelled as such. GLEIF publishes this data under CC0 1.0; this article is not affiliated with GLEIF. On the use of AI: I wrote the probe script and this draft together with Claude Code, then compared every figure in the text against the raw JSON before publishing — which is how the difference from the two-day-old numbers in the table above was caught.