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. OnSnapshotRefreshed and OnCompanyStatusChanged run inside the caller's transaction: an error in your subscriber fails the user's lookup and rolls back the cache write. OnFilingEventReceived runs 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.
  • OnCompanyStatusChanged may 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 calls Message, Confirm or opens a page works in the first case and errors in the second. Treat it like OnFilingEventReceived: 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 CRN and "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. OnFilingEventReceived fires in a background session with no client: no Message, Confirm or 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 var for 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): Boolean normalises the value, prompts for a CRN if it is not valid, resolves it and opens the Snapshot Card; returns false if 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) before Run opens the page with the matches for a name already loaded.
  • page "D2KL Company Officers" and page "D2KL Company Filings" - LoadFor(CRN) and LoadFor(CRN, FromDate) before Run open 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).

Something missing or wrong on this page? Email support@d2k.uk. This page is also available as Markdown.