The D2K Companies House Lookup API
Last updated
This page describes the supported API of D2K Companies House Lookup for developers building a Business Central extension on top of it: the interface, the events, filed documents, filing watches, the data you can read and the pages you can open. It is part of the developer guide, Building on D2K Companies House Lookup, which says what is supported and how to take the dependency.
The interface
codeunit "D2KL Default Resolver" implements interface "D2KL ICH Resolver". Declare a variable of the interface type and assign the codeunit to it, or call the codeunit directly.
| Method | What it does |
|---|---|
ResolveByCRN(CRN: Code[8]; ForceRefresh: Boolean): Record "D2KL Company Snapshot" |
Returns the company profile. Served from the cache when the snapshot is younger than Cache TTL (Profile) in Setup (default 7 days); otherwise fetched from Companies House and cached. ForceRefresh always fetches. Errors if the CRN is malformed or the company does not exist. |
ResolveByCRNWithMaxAge(CRN: Code[8]; MaxAge: Duration): Record "D2KL Company Snapshot" |
As above, but you choose the freshness: cached data is served when younger than MaxAge; zero always fetches. |
GetCompanyStatus(CRN: Code[8]): Enum "D2KL Company Status" |
The company status, refreshed when the cached snapshot is older than Cache TTL (Status) (default 24 hours). |
SearchByName(Name: Text; MaxResults: Integer; var ResultBuffer: Record "D2KL Company Snapshot" temporary) |
Live name search. Clears the buffer, then fills it with up to MaxResults matches (20 when zero or negative). Results are never cached. |
GetRecentFilings(CRN: Code[8]; FromDate: DateTime; var ResultBuffer: Record "D2KL Filing Event" temporary) |
Filing history on or after FromDate; 0DT for the full history. Pages through Companies House 100 filings at a time, up to 1,000. When Cache TTL (Filings) is set (default 1 hour) a dated request is served from filings recorded earlier if they cover the date; a full-history request always goes to Companies House. |
GetOfficers(CRN: Code[8]; var ResultBuffer: Record "D2KL Officer" temporary) |
Current officers. Live and never cached; Companies House returns at most 100 per company. |
IsConfigured(): Boolean |
True when the current company has a Companies House API key stored. Check it before offering a lookup. |
ValidateCRNFormat(CRN: Code[8]): Boolean |
Format check only, no API call: eight characters, either all digits or two letters followed by six digits (SC, NI, OC, SO, NC…). |
CRNs are eight characters and users often type them without the leading zeros. NormaliseCRN on page "D2KL Company Lookup Dialog" removes spaces, upper-cases and left-pads a numeric value, so you can accept whatever a user typed in a Registration Number field; it returns blank for anything longer than eight characters.
Every call that reaches Companies House is a synchronous HTTP round trip from the user's session. Budget for it: typically well under a second, a few seconds when a retry is needed.
Events
codeunit "D2KL Events" publishes three integration events. Your app can subscribe to them; it cannot raise them.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"D2KL Events", OnFilingEventReceived, '', false, false)]
local procedure HandleOnFilingEventReceived(var FilingEvent: Record "D2KL Filing Event")
[EventSubscriber(ObjectType::Codeunit, Codeunit::"D2KL Events", OnCompanyStatusChanged, '', false, false)]
local procedure HandleOnCompanyStatusChanged(CRN: Code[8]; OldStatus: Enum "D2KL Company Status"; NewStatus: Enum "D2KL Company Status")
[EventSubscriber(ObjectType::Codeunit, Codeunit::"D2KL Events", OnSnapshotRefreshed, '', false, false)]
local procedure HandleOnSnapshotRefreshed(var Snapshot: Record "D2KL Company Snapshot")
| Event | When it fires |
|---|---|
OnFilingEventReceived |
Once for each new filing the background poller records for a watched company - exactly once per filing, never for a filing seen before. Not raised by on-demand calls to GetRecentFilings. Runs in the poller's background session. |
OnCompanyStatusChanged |
When a profile refresh finds the status has changed since the last snapshot (Active to Liquidation, say). Never on the first fetch of a company. Can run in the poller's background session, not only in a user's: for a watched company the hourly poll refreshes the profile once the Cache TTL (Status) has lapsed, which is how a status change is noticed without anyone opening the card. Write the subscriber for a background session - see the rules below. |
OnSnapshotRefreshed |
Every time a profile is written from Companies House, first fetch included. Not raised by name searches, which are not cached. Runs in the session that ran the lookup. |
Rules for subscribers:
- Do not raise errors.
OnSnapshotRefreshedandOnCompanyStatusChangedrun inside the caller's transaction: an error in your subscriber fails the user's lookup and rolls back the cache write.OnFilingEventReceivedruns inside the poller's per-company transaction: an error rolls back that company's filings, which are fetched and notified again on the next run. OnCompanyStatusChangedmay have no user interface. It fires in a user's session when someone refreshes a company, and in the poller's background session when a watched company's status is found to have moved. A subscriber that callsMessage,Confirmor opens a page works in the first case and errors in the second. Treat it likeOnFilingEventReceived: record, queue or post, and keep it short. If you need to tell a user, write a record and surface it from a page they open.- Be idempotent. Because of that rollback, a filing can be notified again after a failed run. Key your own records on
CRNand"Source Transaction Id"and ignore what you have already seen. ("Source Transaction Id"is blank for the rare filing Companies House publishes without one; fall back to date, type and description for those.) - No user interface.
OnFilingEventReceivedfires in a background session with no client: noMessage,Confirmor pages. Record, queue or post - and keep it short, because one company's filings are one transaction. - Read, do not write. The records arrive by
varfor efficiency, not for editing. Do not modify or delete them.
Filed documents
Most entries on the register have a PDF behind them - the accounts, the
confirmation statement, the resolution. "Document Id" on D2KL Filing Event
carries the Companies House id for it, taken from the filing's
links.document_metadata, and is blank for the entries that have none.
codeunit "D2KL Filing Document" turns that into a file:
| Member | What it does |
|---|---|
Download(FilingEvent: Record "D2KL Filing Event") |
Fetches the PDF and offers it to the user as a download. Needs a client session - it calls DownloadFromStream - so never call it from a job queue or an event subscriber. A filing with no document says so and makes no API call. |
SuggestedFileName(FilingEvent: Record "D2KL Filing Event"): Text |
The name the download is offered under: 00445790-2026-09-16-Capital-return-purchase-own-shares.pdf. |
The fetch goes through the same rate limiter, retry policy and error mapping as
every other call, and is logged to the API call log as /document/{id}/content.
Companies House does not serve documents from its sandbox, so this errors
clearly when Sandbox Mode is on rather than calling the live host with a
sandbox key.
Filings recorded before this app knew about documents have a blank
"Document Id"; the next fetch for that company fills it in without
duplicating the filing.
Filing watches
codeunit "D2KL Filing Watch Mgt" is the register of companies the background poller monitors.
| Procedure | What it does |
|---|---|
RegisterWatch(CRN: Code[8]; SubscriberApp: Text[80]) |
Starts monitoring the company. Idempotent: an existing watch is re-enabled and its Subscriber App updated. Pass your app's name as SubscriberApp; it is shown on the Filing Watch List so administrators know who asked. The first registration in a company also creates the poller's hourly Job Queue Entry. Errors if the CRN is malformed. |
UnregisterWatch(CRN: Code[8]) |
Removes the watch. Filing events already recorded are kept. Does nothing if there is no watch. |
SetEnabled(CRN: Code[8]; Enabled: Boolean) |
Pauses or resumes an existing watch without removing it. Errors if there is no watch. |
There is one watch per CRN per Business Central company, whoever registered it. If two apps watch the same CRN they share the watch, and pausing or removing it affects both - treat the register as shared state, and prefer leaving a watch in place over removing one you did not create.
How the poller behaves: it runs hourly; for each enabled watch it fetches the company's filings, records the new ones in D2KL Filing Event (new means a Companies House transaction id not seen before for that CRN) and raises OnFilingEventReceived once for each. A watch that has never been polled looks back seven days; after that the window opens at the day before the last successful poll. Last Polled At advances only after a successful fetch, so a rate limit or an outage never loses a window. Each company is its own transaction, committed as it completes: one failing company does not block the others, and a failure is retried on the next run.
Data
All five tables are per company (Business Central company). Read them freely with D2KL Read; never write to them. Everything in them is written by the app's own code with inherent permissions, and the cache depends on the timestamps it maintains.
D2KL Company Snapshot - one row per CRN, the cached profile: CRN, "Registered Name", "Trading Name", "Company Status", "Company Type", "Incorporation Date", "Address Line 1", "Address Line 2", City, County, "Post Code", Country, "Country of Origin" (ISO 3166-1 alpha-2), "Last Refreshed". The remaining fields ("Refresh Lock", "Filings Refreshed At", "Filings Complete From") are cache bookkeeping; ignore them.
D2KL Company Snapshot SIC - SIC codes for a snapshot: CRN, Sequence, "SIC Code", "SIC Description".
D2KL Officer - CRN, "Officer Role", "Officer Name", "Appointed Date", "Resigned Date", Nationality, "DOB Month", "DOB Year", "Last Refreshed". Returned by GetOfficers as a temporary buffer only: officers are not stored in this version, so the table itself is empty. Officer data is personal data (the table is classified EndUserIdentifiableInformation); if you copy it into your own tables, classify it the same way and decide how long you keep it.
D2KL Filing Event - "Entry No.", CRN, "Filing Type", "Filing Date", "Filing DateTime", Description, "Filing Status", "Notification Sent", "Source Transaction Id", "Document Id". Written by the poller and by dated GetRecentFilings calls when the filings TTL is on. A retention policy keeps rows for at least 90 days and an administrator may prune older ones, so it is not an archive: copy what you need into your own tables when the event fires.
D2KL Filing Watch - CRN, "Subscriber App", "Registered At", "Last Polled At", Enabled. Read it to see what is being watched and when it was last polled; change it only through D2KL Filing Watch Mgt.
Enums. All five are Extensible. Extending them is allowed, but the app only ever produces its own values - Companies House data is mapped internally, so a value you add never appears in a record the app wrote. Treat Unknown and Other as the catch-alls, expect new values in minor versions as Companies House adds statuses and filing types, and give every case an else.
Pages
Open the app's own pages from your actions rather than building lookups:
page "D2KL Company Lookup Dialog"-LookupAndShow(RegistrationNumber: Text): Booleannormalises the value, prompts for a CRN if it is not valid, resolves it and opens the Snapshot Card; returnsfalseif the user cancelled. This is the flow behind the Companies House snapshot action on the Customer, Vendor and Contact cards. Nothing is written back to your record.page "D2KL Company Search"-SetSearchText(NewText: Text)beforeRunopens the page with the matches for a name already loaded.page "D2KL Company Officers"andpage "D2KL Company Filings"-LoadFor(CRN)andLoadFor(CRN, FromDate)beforeRunopen the live view for one company.- The Snapshot List and Card, Filing Watch List and Filing Event List open by name:
Page.Run(Page::"D2KL Company Snapshot Card", Snapshot).