Skip to content

2026-09-08

Business verification, and cross-signal consistency

Two new endpoints: POST /api/v1/entity checks a Legal Entity Identifier or a UK company number against registers synced locally, and POST /api/v1/consistency runs any subset of email, phone, IP and address and names where they agree and where they do not.

  • POST /api/v1/entity is the fifth product: a Legal Entity Identifier read against the GLEIF Level 1 golden copy, or a UK company number read against the Companies House free company data product. Both are bulk files synced into a local table, so no third party sits in the request path. The guide is at /docs/business-verification.
  • It answers the legal name, whether the entity is still live (active, inactive, dissolved, with the register's own wording in status_detail), the country and jurisdiction of formation, the entity category and legal form, and the incorporation date. For a LEI it adds checksum_valid — the ISO 7064 MOD 97-10 check digit from ISO 17442 — plus registration_status and next_renewal_at.
  • A LEI can lapse while the company behind it is perfectly alive, so the two facts are separate: status is about the entity, registration_status is about the identifier, and lei_registration_not_current flags the difference.
  • Where a LEI names a Companies House number and both registers are installed, the two records are compared field by field. agreements quotes both sides and flags names name_differs_across_registers, status_differs_across_registers and dissolved_in_national_register. Names are compared on their letters and digits with the company-form words removed, so "ACME HOLDINGS LTD." and "Acme Holdings Limited" agree.
  • The checksum is reported beside the register answer rather than standing in for it. Measured against the published register: of 45,515 consecutive real LEIs, 45,512 satisfy the check digit, and all three that do not are annulled registrations.
  • No registered address is stored or returned, and none will be. A registered address is very often somebody's home, and an identifier alone does not say whether a limited company or a sole trader stands behind it. The product carries country and jurisdiction, which answer whether this is a real, live entity without holding a register of where people live.
  • Monitored lists: save up to 500 identifiers and have them re-checked weekly or monthly (no daily cadence — the UK company file is republished monthly). A run answers every identifier ok, one of left_register, dissolved, inactive, registration_lapsed and registers_disagree, or unverifiable where no register could answer at all.
  • left_register exists because of how the UK register actually works: the Companies House free product is a snapshot of the live register, so a company that is struck off is removed from the file rather than marked dissolved. A dissolution therefore reaches you as the identifier disappearing between runs, and reporting that as unverifiable would have let the single most important event pass silently. An email goes out only for an identifier that was fine last run and is not now; the first run only sets the baseline.
  • Every monitor now prunes itself, across all five products. A saved list that stops being re-checked is deleted along with the identifiers and the results it stored — an email monitor, a phone monitor, an IP monitor, an address monitor and a business monitor alike. A list in active use is never touched, because each run advances last_run_at, so the window only closes on one that has genuinely stopped. Retention is 365 days by default and configurable per product, prunes_after_days is on every monitor payload and meta.retention_days on every list endpoint, so the rule is visible in the API rather than only in the docs.
  • The business monitor is where the reasoning started: an identifier is often a company number but can equally be a sole trader's, and nothing in it says which. The same argument holds for an email address, a phone number, an IP and a postal address, which is why the rule was extended to the other four rather than left as a special case.
  • 1 credit when a register answers. A malformed identifier, one no register carries, an unsupported register and a register that is not yet installed are all free — and a register that is not installed says so, rather than reporting that the company does not exist.
  • Data: the LEI reference data is published under CC0 1.0 and refreshed daily; the Companies House file is Open Government Licence v3.0 and republished monthly, so an answer can be up to five weeks behind the live register. Every answer names both registers with the date of the copy, and /status lists them. Spaw is not affiliated with or endorsed by the organisation that publishes the LEI data.
  • POST /api/v1/consistency takes any subset of email, phone, ip and address, runs each through the product that owns it, and returns data.signals with all four keys always present — each holding that product's full answer, field for field, or null for a signal you did not ask for. The signals run in a fixed order (ip, phone, address, email) because a JSON object has no input order, and because email is the only leg that does DNS and a mailbox handshake. The guide is at /docs/cross-signal-consistency.
  • On top of them, a free consistency block: country_agreement over the phone, address and IP countries, and eight comparisons that each quote both sides — the signal, the field and the value that decided it — so nothing is asserted you cannot see the working for.
  • Seven flags, each with its own reference page: phone_country_differs_from_address_country, ip_country_differs_from_address_country, ip_country_differs_from_phone_country, disposable_email_from_datacenter_ip, disposable_email_from_anonymous_network, mail_drop_address_with_free_email_provider and disposable_phone_with_disposable_email. They sit in a flag field rather than a reason, because they describe two signals rather than one.
  • A comparison is emitted only when both of its signals answered. Where one side is null it answers not_compared with withheld: "missing_value" — a null means not evaluated, never no. Where the IP is a VPN exit, a relay or a datacenter egress, the country comparisons are withheld as low_confidence: that country belongs to the network operator, and reporting a difference would flag everyone who uses a VPN.
  • No score, no risk level, no verdict and no recommendation, and there will not be one. A difference between two fields is a fact about the fields, not a judgement about a person — people travel, move house, keep a foreign number and browse privately.
  • No new dataset, licence or sync command: every comparison is arithmetic over fields the four pipelines already computed. Each signal bills exactly as its own single lookup, meta.credits_used is the sum, and the analysis is free. Running out part-way keeps what was paid for and reports meta.stopped_reason.
  • The MCP server gained verify-entity and check-consistency, and the Python, Node and PHP SDKs gained check_consistency / checkConsistency. Keys carry a new consistency scope, and the endpoint has its own rate-limit bucket, since one call fans out to as many as four lookups.
  • A free /tools/card-number-checker joins the tool pages: brand, Luhn check digit and permitted length, computed entirely in the browser — no request, no endpoint, no log line, and the number never appears in the URL. It makes no issuer, bank, country or debit/credit/prepaid claim, and says plainly why: there is no free, openly-licensed, authoritative source for those, so it answers nothing rather than guessing.
  • Fixes: a mailbox probe lost to a provider outage is now free whatever names the verdict — a role address on an accepts-mail domain was still being charged during an outage, against the published rule. The French national address register (Base Adresse Nationale) is installed alongside the other five, and the Swiss register's coverage no longer claims Liechtenstein, which its file has never contained.

markdown version: /changelog/2026-09-08-business-verification.md · atom feed: /changelog.atom

All releases