DEV Community

Devil Scrapes
Devil Scrapes

Posted on

GLEIF ships two status fields for one company, and they answer different questions

Quick answer

The GLEIF global LEI register returns two different "status" fields on every record, and they answer two different questions. entity.status says whether the legal entity itself is alive: ACTIVE or INACTIVE. registration.status says whether the LEI registration is current: ISSUED, LAPSED, and others. A company can be ACTIVE with a LAPSED LEI โ€” meaning the business is real and operating, but nobody has renewed its identifier โ€” and that combination is itself a compliance signal most integrations never surface, because they read one status field and assume it covers both questions.

Does GLEIF have one status field or two? ๐Ÿ”

Two, and conflating them is the single easiest way to misread a KYC screen against this register. Pull apart one real record and you get:

{
  "entity": { "status": "ACTIVE" },
  "registration": { "status": "LAPSED" }
}
Enter fullscreen mode Exit fullscreen mode

entity.status reflects the company's own legal standing โ€” is it still a going concern. registration.status reflects the administrative state of the LEI code itself โ€” has it been renewed on schedule with an accredited registration agent. These move independently. An active, operating company can let its LEI lapse simply by not renewing it on time; that says nothing about whether the business still exists, but it says a great deal about whether the company is currently meeting a regulatory reporting obligation that requires a valid LEI. A screening process that reads entity.status == "ACTIVE" and calls it done will pass a counterparty whose LEI has been lapsed for two years โ€” exactly the kind of gap a reporting-deadline check exists to catch.

The GLEIF LEI Scraper surfaces both fields on every row, named to make the distinction obvious rather than collapsing them into one ambiguous status column:

status: str | None                  # entity status โ€” ACTIVE or INACTIVE
registration_status: str | None     # LEI status โ€” e.g. ISSUED, LAPSED
Enter fullscreen mode Exit fullscreen mode

Are the name and legal-form fields always plain strings? ๐Ÿงพ

Not reliably โ€” and a parser that assumes they are will crash or silently drop real values. GLEIF's JSON:API wraps some text fields as objects rather than bare strings:

{ "name": "Equinor ASA", "language": "en" }
Enter fullscreen mode Exit fullscreen mode

instead of the plain "Equinor ASA" a naive schema might expect. This shows up on legalName and legalForm, and it isn't consistent enough across every record to hardcode either shape:

def _text(value):
    """GLEIF wraps some strings as {"name": ..., "language": ...}."""
    if isinstance(value, dict):
        return value.get("name") or value.get("id")
    if isinstance(value, str):
        return value or None
    return None
Enter fullscreen mode Exit fullscreen mode

Handling both shapes uniformly means never having to pin down in advance which records will arrive wrapped and which won't โ€” the same defensive posture the address block needs, since legalAddress.addressLines arrives as an array of lines rather than a single string, and registrars leave it sparse often enough that joining whatever lines exist (and leaving the field empty rather than inventing a street) is the only safe default.

Can I just page through the whole register to find what I need? ๐Ÿ“–

No, deliberately not. GLEIF's register holds roughly 3.4 million LEI records, and paging that unfiltered isn't a request the API is built to serve efficiently or a request this Actor will send:

def has_filter(self) -> bool:
    """GLEIF will not page 3.4M records unfiltered, so require a filter."""
    return bool(self.legal_name or self.country)
Enter fullscreen mode Exit fullscreen mode

A legal-name match or a country filter is required before the first request goes out โ€” validated before any network call, so an unfilterable query fails immediately rather than after burning through pages that were never going to reach the record you wanted. The actual engineering effort is entirely in shaping the query correctly and flattening what comes back, which for a JSON:API service means bracketed parameter keys most REST clients don't expect by default:

params = {
    "page[size]": 100,
    "page[number]": page,
    "filter[entity.legalName]": legal_name,
    "filter[entity.legalAddress.country]": country,
}
Enter fullscreen mode Exit fullscreen mode

The part that generalizes ๐Ÿงญ

Two fields with the word "status" in their name are not the same field. Any register or API tracking both an entity and a credential issued to that entity โ€” a license, a certification, an identifier โ€” is a candidate for exactly this trap. Check whether "status" describes the thing or the paperwork about the thing before building a filter on it.

A value that's usually a string but is sometimes an object will eventually be an object on the record you didn't test with. Defensive unwrapping at the parse boundary costs a few lines once; a hardcoded .get("legalName") that assumes a bare string costs a production AttributeError on whichever registrar wraps its data differently.

What the Actor gives you

One row per LEI record, flattened out of JSON:API's nested attributes.entity/attributes.registration shape:

  • LEI code, legal name, legal form, jurisdiction, entity category
  • both status fields, named distinctly โ€” entity status and LEI registration status
  • registered address, joined from GLEIF's address-line array, plus city/region/country/postal code
  • associated BIC/SWIFT codes, local business-register identifier, and the full registration date trail: initial registration, last update, next renewal
  • retries on transient 429/5xx instead of failing the whole run over a single blip

The honest limitations ๐Ÿšง

Register record only โ€” no ownership hierarchy, no financial data, no beneficial-ownership chain. BIC codes are present only for entities that registered them with GLEIF; their absence isn't an error. Queries require a legal name or country filter; the register won't page 3.4 million records unfiltered.

FAQ

What's the difference between status and registration_status?
status (entity.status) is whether the legal entity itself is active or inactive. registration_status is whether its LEI registration is current โ€” ISSUED, LAPSED, and other administrative states. They can and do disagree.

Why is a legal name sometimes returned as an object instead of a plain string?
GLEIF's own API wraps some text values as {"name": ..., "language": ...}. This Actor unwraps both shapes to a plain string automatically.

Can I search for one specific LEI code?
Search by legal name and filter the results, or narrow by country โ€” GLEIF's API matches on legal name text, not by direct LEI lookup in this Actor's current input.

Do I need an API key?
No. GLEIF publishes the register openly with no credentials required.

Pricing

$0.20 per run plus $0.002 per result row โ€” about $2.20 per 1,000 results. No subscription, no minimum, no card to start.

โ†’ GLEIF LEI Scraper on Apify


Built by Devil Scrapes. We handle the two status fields, the wrapped strings, and the JSON:API bracket syntax, so you get a flat table instead of a weekend.

Top comments (0)