A .NET client for the Companies House Public Data API.
Upgrading from an earlier version? This is a major, deliberately breaking rewrite. See MIGRATION.md for the full list of changes and before/after snippets.
Two NuGet packages are published:
# The core client and all request/response models
dotnet add package CompaniesHouse
# Optional: DI helpers for ASP.NET Core / generic-host apps
dotnet add package CompaniesHouse.Extensions.Microsoft.DependencyInjectionBoth packages multi-target net8.0, net9.0 and net10.0.
Register an application on the Companies House developer hub to get an API key for the public data API.
using CompaniesHouse;
var settings = new CompaniesHouseSettings(apiKey);
using var client = new CompaniesHouseClient(settings);CompaniesHouseClient implements IDisposable - always dispose it (or wrap it
in a using block) once you're done, since it owns an underlying HttpClient.
You can also construct the client from your own HttpClient (useful in tests,
or when you want full control over handlers/base address):
using var httpClient = new HttpClient { BaseAddress = CompaniesHouseUris.Default };
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:")));
using var client = new CompaniesHouseClient(httpClient);Install CompaniesHouse.Extensions.Microsoft.DependencyInjection and register
the client on your IServiceCollection. Several overloads are available,
built on IOptions<CompaniesHouseClientOptions>:
// Simplest - just an API key
services.AddCompaniesHouseClient(apiKey);
// A custom base URI (e.g. against a sandbox/test host)
services.AddCompaniesHouseClient(new Uri("https://api.company-information.service.gov.uk/"), apiKey);
// Full control via a delegate
services.AddCompaniesHouseClient(options =>
{
options.ApiKey = apiKey;
options.BaseUri = CompaniesHouseUris.Default;
});
// Bind from IConfiguration (defaults to the "CompaniesHouse" section)
services.AddCompaniesHouseClient(configuration);Every overload also accepts an optional configureHttpClientBuilder delegate,
letting you customise the underlying IHttpClientBuilder (e.g. to add Polly
resilience handlers):
services.AddCompaniesHouseClient(apiKey, builder => builder.AddStandardResilienceHandler());Once registered, inject ICompaniesHouseClient - the main facade interface -
into your dependencies:
public class MyPageModel(ICompaniesHouseClient client) : PageModel
{
// ...
}Document endpoints (GetDocumentMetadataAsync/DownloadDocumentAsync) talk to
a separate host and are registered independently via
AddCompaniesHouseDocumentClient, with the same set of overloads.
{
"CompaniesHouse": {
"ApiKey": "your-api-key",
"BaseUri": "https://api.company-information.service.gov.uk/"
}
}services.AddCompaniesHouseClient(builder.Configuration);Every "enum" in the Companies House API (company status, officer role, charge
type, etc.) is modelled as a string-backed, readonly record struct
rather than a plain C# enum. This is deliberate: Companies House regularly
adds new wire values, and a plain enum throws (or silently defaults) the
moment it sees one it doesn't recognise. See the
design rationale
for the full background.
CompanyStatus status = companyProfile.CompanyStatus;
status.Value; // the raw wire value, e.g. "active"
status.HasValue; // false only for the default/absent value
status.IsKnown; // true if this library recognises the value
status.Description; // a friendly description for known values, e.g. "Active"Compare against the generated static members (CompanyStatus.Active,
CompanyStatus.Dissolved, ...) rather than raw strings, and always keep a
fallback arm for values you don't recognise yet:
var description = status switch
{
_ when status == CompanyStatus.Active => "is active",
_ when status == CompanyStatus.Dissolved => "is dissolved",
_ when status.IsKnown => status.Description,
_ => $"unrecognised status: {status.Value}", // never throws
};New values ship as a new minor version of the CompaniesHouse package (the
value types are generated from the official
api-enumerations data)
- you never need to hand-edit or wait on a code change to keep deserializing.
Every client method returns a CompaniesHouseResponse<T> — a discriminated
union whose concrete subtype tells you exactly what happened:
| Subtype | When | Extra property |
|---|---|---|
Success |
2xx | Data (non-null), Headers |
NotFound |
404 | — |
RateLimited |
429 | RetryAfter |
Unauthorized |
401/403 | — |
ClientError |
other 4xx | — |
ServerError |
5xx | RetryAfter |
All subtypes expose StatusCode and ReasonPhrase. Transport failures (network
errors, DNS, timeout) propagate as HttpRequestException from the underlying
HttpClient.
Call .Data directly — it returns the deserialized body on Success and throws
InvalidOperationException for every other subtype, so you never silently get
null:
var company = (await client.GetCompanyProfileAsync(companyNumber)).Data;
Console.WriteLine(company.CompanyName);Use a switch expression when you need to handle specific outcomes:
var result = await client.GetCompanyProfileAsync(companyNumber);
var message = result switch
{
CompaniesHouseResponse<CompanyProfile>.Success { Data: var company } =>
$"Found: {company.CompanyName}",
CompaniesHouseResponse<CompanyProfile>.NotFound =>
"Company not found.",
CompaniesHouseResponse<CompanyProfile>.RateLimited { RetryAfter: var delay } =>
$"Rate limited — retry after {delay}.",
CompaniesHouseResponse<CompanyProfile>.Unauthorized =>
"Check your API key.",
CompaniesHouseResponse<CompanyProfile>.ServerError { StatusCode: var code, RetryAfter: var delay } =>
$"Server error {code} — retry after {delay}.",
_ => $"Unexpected response: {result.StatusCode}",
};var request = new SearchAllRequest
{
Query = "Jay2Base",
StartIndex = 0,
ItemsPerPage = 10
};
var result = await client.SearchAllAsync(request);
foreach (var item in result.Data.Items)
{
// Do something...
}For a specific resource type, use SearchCompanyAsync, SearchOfficerAsync,
SearchDisqualifiedOfficerAsync, SearchCompaniesAlphabeticallyAsync,
SearchDissolvedCompaniesAsync or AdvancedCompanySearchAsync with the
matching request type.
var companies = await client.SearchCompanyAsync(new SearchCompanyRequest { Query = "Jay2Base" });
var officers = await client.SearchOfficerAsync(new SearchOfficerRequest { Query = "Jay2Base" });
var disqualified = await client.SearchDisqualifiedOfficerAsync(new SearchDisqualifiedOfficerRequest { Query = "Jay2Base" });var result = await client.GetCompanyProfileAsync("10440441");result is a NotFound subtype if there was no match for that company number.
var result = await client.GetOfficersAsync("03977902");
// Optionally page the results
var page = await client.GetOfficersAsync("03977902", startIndex: 10, pageSize: 10);A single officer appointment can be fetched directly:
var officer = await client.GetOfficerByAppointmentIdAsync("03977902", appointmentId);var result = await client.GetAppointmentsAsync(officerId, startIndex: 0, pageSize: 25);var result = await client.GetCompanyFilingHistoryAsync("10440441", startIndex: 0, pageSize: 25);
var item = await client.GetFilingHistoryByTransactionAsync("10440441", transactionId);var result = await client.GetCompanyInsolvencyInformationAsync("10440441");result is a NotFound subtype if there is no insolvency information for the company.
var result = await client.GetPersonsWithSignificantControlAsync("10440441", startIndex: 0, pageSize: 25);var charges = await client.GetChargesListAsync("10440441", startIndex: 0, pageSize: 25);
var charge = await client.GetChargeByIdAsync("10440441", chargeId);var result = await client.GetRegisteredOfficeAddress("10440441");var metadata = await client.GetDocumentMetadataAsync("FIxRR8teCKodjkBLRDHv2Cb8y0-nQ7T5G3BEXfWtOu4");
var document = await client.DownloadDocumentAsync("FIxRR8teCKodjkBLRDHv2Cb8y0-nQ7T5G3BEXfWtOu4");metadata/document is a NotFound subtype if there was no metadata/document for the given id.
More endpoints land progressively - see .plans/ for what's in flight.
A runnable end-to-end example, covering direct construction, DI registration,
search, company profile, officers, and gracefully handling an unrecognised
enum value, lives in samples/SampleProject.
- Fork
- Hack!
- Pull Request
See AGENTS.md for repository conventions, build/test commands and the design decisions behind the v-next rewrite.
NuGet publishing is driven by the CI Docker build, which produces the final
.nupkg artifacts in ./artifacts. Before push-to-NuGet, CI validates that
each package contains README.md and has nuspec <readme>README.md</readme>
metadata so NuGet.org renders the project README correctly.
dotnet restore
dotnet build -c Release
dotnet test -c ReleaseIntegration tests hit the real Companies House API and need an API key in the
COMPANIES_HOUSE_API_KEY environment variable - they're skipped/fail without
one, which is expected when working offline.