The library provides an async client (:py:class:`ch_api.api.Client`) that returns typed Pydantic models and handles pagination automatically.
The client requires a Companies House API key for authentication. Here's a simple example:
import asyncio
from ch_api import Client, api_settings
async def main() -> None:
auth = api_settings.AuthSettings(api_key="your-api-key-here")
client = Client(credentials=auth)
# Get a company profile
company = await client.get_company_profile("09370755")
print(f"Company: {company.company_name}")
print(f"Status: {company.company_status}")
if __name__ == "__main__":
asyncio.run(main())Or using the test client with run_async_func:
>>> async def get_company_demo(client):
... company = await client.get_company_profile("09370755")
... return company is not None
>>> run_async_func(get_company_demo)
True
The client returns rich Pydantic models defined in :mod:`ch_api.types` and uses :class:`ch_api.types.pagination.types.MultipageList` for all paginated results. See :doc:`api-reference` for the full API surface.
All requests to the Companies House API require authentication via an API key. You can obtain an API key by:
- Registering for a Companies House account
- Creating an application in your account
- Obtaining the API key for your application
Pass your API key to the client using :class:`ch_api.api_settings.AuthSettings`:
from ch_api import Client, api_settings
auth = api_settings.AuthSettings(api_key="your-api-key-here")
client = Client(credentials=auth)The company profile contains core information about a company:
>>> async def company_profile_example(client):
... company = await client.get_company_profile("09370755")
... return company.company_name is not None and company.company_number is not None
>>> run_async_func(company_profile_example)
True
Get information about company officers (directors, secretaries, etc.):
>>> async def officers_example(client):
... officers = await client.get_officer_list("09370755")
... return len(officers.data) >= 1
>>> run_async_func(officers_example)
True
Get information about persons with significant control:
>>> async def psc_example(client):
... result = await client.get_company_psc_list("09370755")
... return result is not None
>>> run_async_func(psc_example)
True
Access a company's filing history:
# Get up to 100 filings (issues several requests as needed)
filings = await client.get_company_filing_history("09370755", result_count=100)
for filing in filings.data:
print(f"Description: {filing.description}")
print(f"Date: {filing.date}")
print(f"Category: {filing.category}")Get information about charges registered against a company:
# Get charges (single response, not paginated)
charges = await client.get_company_charges("09370755")
for charge in charges.items or []:
print(f"Charge Number: {charge.charge_number}")
print(f"Created: {charge.created_on}")
print(f"Status: {charge.status}")
print(f"Persons Entitled: {charge.persons_entitled}")Search for companies by name:
>>> async def search_companies_example(client):
... results = await client.search_companies("Apple")
... return len(results.data) >= 1
>>> run_async_func(search_companies_example)
True
Use advanced search with multiple criteria:
from ch_api.types.public_data.search_companies import CompanySearchQuery
# Advanced search with filters
results = await client.advanced_company_search(
company_name_includes="tech",
company_status="active",
company_type="ltd",
location="London",
result_count=100,
)
for company in results.data:
print(f"{company.company_name} ({company.company_number})")Search for officers across all companies:
results = await client.search_officers("John Smith", result_count=100)
for officer in results.data:
print(f"Name: {officer.title}")
print(f"Date of Birth: {officer.date_of_birth}")
# Note: Appointments not included in search results
# Use get_officer_appointments() for detailsSearch for disqualified officers:
results = await client.search_disqualified_officers("Smith", result_count=100)
for officer in results.data:
print(f"Name: {officer.title}")
print(f"Date of Birth: {officer.date_of_birth}")Many endpoints return a :class:`ch_api.types.pagination.types.MultipageList`: an immutable value object with data (this call's items, a tuple) and pagination (cursor metadata). Pass result_count to collect at least that many items in one call (the client issues multiple page_size requests as needed).
Fetching a page:
>>> async def lazy_loading_example(client):
... results = await client.search_companies("tech")
... return len(results.data) >= 1
>>> run_async_func(lazy_loading_example)
True
Pass result_count to collect more items in one call, then walk the rest of
the result set with client.fetch_next_page:
# Collect at least 100 items (may issue several underlying requests)
page = await client.search_companies("tech", result_count=100)
while True:
for company in page.data:
print(company.title)
if not page.pagination.has_next:
break
# fetches the next batch of result_count items
page = await client.fetch_next_page(page.pagination.next_page)
# Or resume statelessly with the opaque cursor token (see "Restarting from a
# token" below) — endpoints themselves take no next_page argument
page = await client.search_companies("tech", result_count=25)
while page.pagination.has_next:
page = await client.fetch_next_page(page.pagination.next_page)pagination.next_page is self-contained: it embeds the endpoint and its
arguments, so a separate process can resume from just the token via
:meth:`~ch_api.api.Client.fetch_next_page` — no in-memory state, no re-supplying the
query. Ideal for an async service or agent tool that returns a page plus a cursor
and continues on a later, independent request:
# First request: return a page and a cursor to the caller
page = await client.search_companies("tech", page_size=20)
payload = {"items": [c.model_dump() for c in page.data],
"next": page.pagination.next_page} # opaque token
# ... later, a fresh request arrives carrying only `next` ...
page2 = await client.fetch_next_page(payload["next"])Only the 12 paginated endpoints can be resumed this way; a token naming anything else is rejected. Configure a :class:`~ch_api.types.pagination.types.PageTokenSerializer` on the client to sign or encrypt the token before it leaves your service.
The Companies House API has rate limits (600 requests per 5 minutes). You can integrate rate limiting using an async rate limiter:
from asyncio_throttle import Throttler
# Allow 600 requests per 5 minutes (300 seconds)
throttler = Throttler(rate_limit=600, period=300)
async def limiter():
async with throttler:
yield
# Pass the limiter to the client
client = Client(credentials=auth, limiter=limiter)
# Now all requests will be rate limited
company = await client.get_company_profile("09370755")The library provides custom exceptions and uses httpx exceptions for HTTP errors:
import httpx
from ch_api.exc import CompaniesHouseApiError, UnexpectedApiResponseError
# get_company_profile returns None for not found (404)
company = await client.get_company_profile("00000000")
if company is None:
print("Company not found")
# Other operations may raise HTTPStatusError
try:
results = await client.search_companies("test")
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
print("Rate limit exceeded - wait before retrying")
elif e.response.status_code == 401:
print("Authentication error")
except UnexpectedApiResponseError as e:
print(f"Unexpected API response: {e}")
except CompaniesHouseApiError as e:
print(f"API error: {e}")You can provide your own httpx.AsyncClient for advanced HTTP configuration:
import httpx
# Create custom session with timeout and connection limits
session = httpx.AsyncClient(
timeout=30.0,
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
)
client = Client(
credentials=auth,
api_session=session
)
# Remember to close the session when done
await session.aclose()Companies House provides a sandbox environment for testing. Use the test settings:
from ch_api import api_settings
# Use sandbox environment
auth = api_settings.AuthSettings(api_key="test-key")
client = Client(
credentials=auth,
settings=api_settings.TEST_API_SETTINGS
)
# The sandbox has a test data generator
from ch_api.types.test_data_generator import CreateTestCompanyRequest
request = CreateTestCompanyRequest(
company_type="ltd",
company_status="active"
)
response = await client.create_test_company(request)
print(f"Created test company: {response.company_number}")
print(f"Auth code: {response.auth_code}")
# You can now use this company number for testing
company = await client.get_company_profile(response.company_number)All methods and return types are fully typed, providing excellent IDE support:
# Your IDE will show:
# - Available methods on the client
# - Expected parameter types
# - Return type information
# - Available fields on returned models
company = await client.get_company_profile("09370755")
# IDE knows company is a CompanyProfile instance
# and will autocomplete its fields:
print(company.company_name) # str
print(company.date_of_creation) # datetime.date
print(company.company_status) # strAll Pydantic models support conversion to dictionaries:
company = await client.get_company_profile("09370755")
# Convert to dictionary
company_dict = company.model_dump()
# Convert to JSON
company_json = company.model_dump_json()
# For paginated results
results = await client.search_companies("Apple", result_count=25)
# Convert all items to dictionaries
companies_list = [c.model_dump() for c in results.data]import asyncio
from ch_api import Client, api_settings
async def find_companies_and_officers():
auth = api_settings.AuthSettings(api_key="your-api-key")
client = Client(credentials=auth)
# Search for companies
companies = await client.search_companies("Technology Ltd", result_count=5)
for company in companies.data:
print(f"\\nCompany: {company.title} ({company.company_number})")
print(f"Status: {company.company_status}")
# Get officers for each company
try:
officers = await client.get_officer_list(company.company_number, result_count=100)
print("Officers:")
for officer in officers.data:
print(f" - {officer.name} ({officer.officer_role})")
except Exception as e:
print(f" Error getting officers: {e}")
if __name__ == "__main__":
asyncio.run(find_companies_and_officers())import asyncio
import json
from ch_api import Client, api_settings
async def export_company_data(company_number: str, output_file: str):
auth = api_settings.AuthSettings(api_key="your-api-key")
client = Client(credentials=auth)
# Gather all company data
data = {}
# Company profile
data['profile'] = (await client.get_company_profile(company_number)).model_dump()
# Officers
officers = await client.get_officer_list(company_number, result_count=200)
data['officers'] = [o.model_dump() for o in officers.data]
# PSCs
psc_result = await client.get_company_psc_list(company_number, result_count=200)
data['pscs'] = [p.model_dump() for p in psc_result.data]
# Filing history (first 100)
filings = await client.get_company_filing_history(company_number, result_count=100)
data['filing_history'] = [f.model_dump() for f in filings.data]
# Write to file
with open(output_file, 'w') as f:
json.dump(data, f, indent=2, default=str)
print(f"Data exported to {output_file}")
if __name__ == "__main__":
asyncio.run(export_company_data("09370755", "company_data.json"))import asyncio
from datetime import datetime, timedelta
from ch_api import Client, api_settings
async def monitor_recent_filings(company_number: str):
auth = api_settings.AuthSettings(api_key="your-api-key")
client = Client(credentials=auth)
# Get filing history
filings = await client.get_company_filing_history(company_number, result_count=100)
# Filter for recent filings (last 30 days)
cutoff_date = datetime.now().date() - timedelta(days=30)
print(f"Recent filings for {company_number}:\\n")
for filing in filings.data:
if filing.date >= cutoff_date:
print(f"Date: {filing.date}")
print(f"Description: {filing.description}")
print(f"Category: {filing.category}")
print()
if __name__ == "__main__":
asyncio.run(monitor_recent_filings("09370755"))- Use Rate Limiting: Always implement rate limiting to avoid hitting API limits
- Handle Errors Gracefully: Wrap API calls in try/except blocks to handle failures
- Use Lazy Pagination: Don't fetch all pages unless you need all data
- Close Sessions: If using a custom session, remember to close it
- Cache Results: Consider caching frequently accessed data to reduce API calls
- Test with Sandbox: Use the sandbox environment for development and testing
- Validate Input: Use the provided type hints and validation to catch errors early
- Monitor Rate Limits: Track your API usage to stay within limits
If you get HTTP 429 errors:
- Implement rate limiting (see :ref:`usage.rate-limiting`)
- Reduce the number of requests per minute
- Wait 5 minutes before retrying after hitting the limit
If you get HTTP 401 errors:
- Check your API key is correct
- Ensure you're not accidentally using test credentials in production
- Verify your API key hasn't expired
If you get HTTP 404 errors:
- Verify the company number is correct (8 characters, may include leading zeros)
- Check if the company has been dissolved or struck off
- Ensure you're using the correct format (e.g., "09370755" not "9370755")
If pagination isn't working as expected:
- Use a regular
forloop overresult.data(it's a tuple) - Pass
result_countto collect more items per call;page_sizecontrols the underlying per-request size - Call
await client.fetch_next_page(result.pagination.next_page)to fetch the next batch