DEV Community

clearpath-data
clearpath-data

Posted on

A daily list of new UK companies by SIC code with the Companies House API

Every UK company declares what it does with one or more SIC codes when it registers. That makes the Companies House register a neat source for a simple question: which companies in my industry were incorporated yesterday? Market research, local-economy reporting, a "new in town" newsletter, or just curiosity all start there.

Companies House has a free API with an advanced search endpoint that filters by incorporation date and SIC code. This post builds a small script that:

  1. finds the right SIC codes for your industry;
  2. pulls every company incorporated on a given day with those codes;
  3. writes a CSV and a short summary;
  4. runs once a day.

Python 3.10+, requests, and PyYAML for the SIC lookup.

Step 0: a free API key

Register on the Companies House Developer Hub, create an application, and add a REST API key to it. It's free.

Authentication is HTTP Basic with a twist. From the authorisation guide: "the Companies House API takes the username as the API key and ignores the password, so can be left blank."

Keep the key out of your code:

export COMPANIES_HOUSE_API_KEY=your-key-here
Enter fullscreen mode Exit fullscreen mode

Step 1: find your SIC codes

SIC codes are five digits (the SIC 2007 scheme): 62012 is "Business and domestic software development", 56101 is "Licensed restaurants". Companies House publishes the descriptions it uses in its public api-enumerations repository, so you can search them from Python. (The human-readable version is Companies House's condensed SIC list.)

# sic_lookup.py  -- usage: python sic_lookup.py software
import sys

import requests
import yaml

URL = "https://raw.githubusercontent.com/companieshouse/api-enumerations/master/constants.yml"


def sic_descriptions() -> dict[str, str]:
    data = yaml.safe_load(requests.get(URL, timeout=30).text)
    # The file also holds older 4-digit SIC 2003 codes; current filings use 5-digit SIC 2007.
    return {str(k): v for k, v in data["sic_descriptions"].items() if len(str(k)) == 5}


if __name__ == "__main__":
    names = sic_descriptions()
    words = [w.lower() for w in sys.argv[1:]]
    for code, desc in sorted(names.items()):
        if all(w in desc.lower() for w in words):
            print(code, desc)
Enter fullscreen mode Exit fullscreen mode
$ python sic_lookup.py software
46510 Wholesale of computers, computer peripheral equipment and software
47410 Retail sale of computers, peripheral units and software in specialised stores
58290 Other software publishing
62011 Ready-made interactive leisure and entertainment software development
62012 Business and domestic software development

$ python sic_lookup.py restaurant
56101 Licensed restaurants
56102 Unlicensed restaurants and cafes
Enter fullscreen mode Exit fullscreen mode

Step 2: the advanced search endpoint

GET https://api.company-information.service.gov.uk/advanced-search/companies takes, among others:

Parameter What it does
incorporated_from, incorporated_to date range (YYYY-MM-DD); use the same date for one day
sic_codes comma-separated list; a company matches if it has any of them
size results per page, documented as "a range of 1 to 5000"
start_index offset for paging

Two details from the docs that the code below handles:

  • A 404 on this endpoint is documented as "No companies found", so treat it as an empty result, not an error.
  • The rate limit is "up to 600 requests within a five-minute period", after which you get 429 until the window resets. One day of one industry is usually a single request, so you're very unlikely to hit it, but a backfill over many days could.
# new_companies.py
import argparse
import collections
import csv
import datetime as dt
import os
import pathlib
import time

import requests

API = "https://api.company-information.service.gov.uk"
PAGE_SIZE = 5000  # documented maximum for advanced search ("a range of 1 to 5000")

session = requests.Session()
# The API key is the Basic-auth username; the password is empty.
session.auth = (os.environ["COMPANIES_HOUSE_API_KEY"], "")


def get(path: str, params: dict) -> dict:
    for _ in range(3):
        r = session.get(API + path, params=params, timeout=30)
        if r.status_code == 429:  # 600 requests per 5-minute window
            reset = int(r.headers.get("X-RateLimit-Reset", time.time() + 60))
            time.sleep(max(reset - time.time(), 1))
            continue
        if r.status_code == 404:  # documented as "No companies found"
            return {"hits": 0, "items": []}
        r.raise_for_status()
        return r.json()
    raise RuntimeError("Rate-limited three times in a row")


def new_companies(day: dt.date, sic_codes: list[str]) -> list[dict]:
    """All companies incorporated on `day` with any of `sic_codes`."""
    rows, start = [], 0
    while True:
        page = get("/advanced-search/companies", {
            "incorporated_from": day.isoformat(),
            "incorporated_to": day.isoformat(),
            "sic_codes": ",".join(sic_codes),
            "size": PAGE_SIZE,
            "start_index": start,
        })
        items = page.get("items", [])
        for c in items:
            addr = c.get("registered_office_address", {})
            rows.append({
                "company_number": c["company_number"],
                "company_name": c["company_name"],
                "incorporated": c.get("date_of_creation", ""),
                "company_type": c.get("company_type", ""),
                "sic_codes": " ".join(c.get("sic_codes", [])),
                "locality": addr.get("locality", ""),
                "postcode": addr.get("postal_code", ""),
                "url": f"https://find-and-update.company-information.service.gov.uk/company/{c['company_number']}",
            })
        start += len(items)
        if not items or start >= page.get("hits", 0):
            return rows
Enter fullscreen mode Exit fullscreen mode

Each item in the response already carries the company number, name, status, type, date_of_creation, SIC codes and registered office address, so there's no need for a second call per company.

Step 3: write the CSV and a summary

def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--sic", required=True, help="comma-separated SIC 2007 codes, e.g. 62012,62020")
    p.add_argument("--date", type=dt.date.fromisoformat, default=dt.date.today() - dt.timedelta(days=1))
    p.add_argument("--out-dir", default="data")
    args = p.parse_args()

    sic = [s.strip() for s in args.sic.split(",") if s.strip()]
    rows = new_companies(args.date, sic)

    out = pathlib.Path(args.out_dir) / f"new-companies-{args.date}-{'-'.join(sic)}.csv"
    out.parent.mkdir(parents=True, exist_ok=True)
    with out.open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0]) if rows else ["company_number"])
        w.writeheader()
        w.writerows(rows)
    print(f"{len(rows)} companies incorporated on {args.date} with SIC {', '.join(sic)} -> {out}")
    towns = collections.Counter(r["locality"].strip().title() for r in rows if r["locality"])
    for town, n in towns.most_common(5):
        print(f"  {town:<20} {n}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Running it for software companies (custom software 62012 plus IT consultancy 62020) on Friday 23 October 2026:

$ python new_companies.py --sic 62012,62020 --date 2026-10-23
215 companies incorporated on 2026-10-23 with SIC 62012, 62020 -> data/new-companies-2026-10-23-62012-62020.csv
  London               97
  Manchester           5
  Brighton             3
  Bury St. Edmunds     3
  Leicester            3
Enter fullscreen mode Exit fullscreen mode

The first rows of the CSV (some columns trimmed):

company_number,company_name,incorporated,sic_codes,locality
17536836,ORBON TECHNOLOGIES LTD,2026-10-23,62012 62090,Romford
17536871,BARLLAST TECHNOLOGIES LTD,2026-10-23,62012 62020 62090 63110,London
17536870,STALVIX LTD,2026-10-23,62020 62090,London
Enter fullscreen mode Exit fullscreen mode

Notice the extra codes: a company can choose up to four SIC codes, and the filter matches on any of them, so 62090 and 63110 show up alongside the ones you asked for.

And restaurants and cafés for the day before:

$ python new_companies.py --sic 56101,56102 --date 2026-10-22
70 companies incorporated on 2026-10-22 with SIC 56101, 56102 -> data/new-companies-2026-10-22-56101-56102.csv
Enter fullscreen mode Exit fullscreen mode

Step 4: run it every day

The default --date is yesterday. Running for today gives a partial list, because companies keep being registered through the day. A crontab line that runs at 07:00 and reads the key from a file only you can read:

0 7 * * * cd /home/me/new-companies && COMPANIES_HOUSE_API_KEY=$(cat ~/.ch_key) /usr/bin/python3 new_companies.py --sic 62012,62020 >> run.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Don't skip weekends. Companies are incorporated at weekends too: on Sunday 25 October 2026 there were 34 new companies with SIC 62012, against 156 on Friday 23 October.

Things to watch

  • SIC codes are self-declared. A company picks its own codes at registration, and some pick a broad one like 62090 ("Other information technology service activities") or 82990. Expect some noise and, for a tight list, filter by name or check the companies by hand.
  • Personal data. For many small companies the registered office is an accountant's or a director's home address. The script keeps only town and postcode and never fetches officers. If you go further (officer names, full addresses), you're processing personal data and UK GDPR applies, even though the register is public.
  • Paging. If you query a wide date range or many codes, hits can exceed one page. The loop follows start_index until it has everything.

This article was written with the help of AI. All code was run against the live Companies House API on 26 October 2026, and the output shown is real (trimmed where noted).

Top comments (0)