Follow register changes
Poll the register-event feed on created_at and never miss a change, across every company.
The shape of it
every few minutes → GET /events?created_at[gte]=<T − 10 min> → page to the end → dedupe by id → T = newest created_atGET /events lists what the register courts entered for any company: a new managing
director, a moved registered office, a changed purpose, a capital increase, an insolvency, a
deletion. Each item is the same company_event that
GET /companies/{id}/events lists for one
company, with the company named in company, so you need no extra call to know whose it is.
A company's first entry, its registration, is not in this list. New companies are
GET /formations.
GET /company-events is the previous name of this list. It keeps answering exactly as GET /events
does, at the same price, and is deprecated: its answers carry a Deprecation header, and a new
integration should call GET /events.
Events, not entries
One register entry can carry several events. An entry that removes one managing director and
appoints another is two company.officer.* events with the same register_entry_number, and
officer_role tells you the role (managing_director, authorized_signatory for a Prokura,
liquidator, …). Group by company.id and register_entry_number if you want one row per entry.
The type says what the entry did, read from its text: a deletion is company.deregistered, a
change to the articles of association is company.articles.changed, insolvency proceedings are
company.insolvency.recorded. An entry that only repeats the registered office or the name already
on file is not published.
Why not poll on effective_date
The list is ordered by the register day (effective_date), newest first. That is the order you
want to read, and the wrong one to poll on:
- Late arrivals. About 7 % of entries reach Registercheck one or more days after their register day. They sort into the middle of the list, below rows you already read, so paging from the top stops before it reaches them.
- The register records a day, not a time. Two polls on the same
effective_datecannot tell what is new.
created_at is the moment an event became available through the API. Every event you have not seen
yet has a created_at later than your last poll, whatever its register day. It moves only when an
event is served again: the register rewrote the entry (its type, day, entry number or the entry
before it), and the event comes back under the same id with the new values. An event that no longer
stands for a change (it now restates an earlier entry, or its day left the 180-day window) is no
longer served; GET /events/{id} answers 404 for it.
Poll
from datetime import datetime, timedelta, timezone
OVERLAP = timedelta(minutes=10)
def poll(since: datetime, seen: dict[str, str]) -> datetime:
newest = since
params = {"created_at[gte]": (since - OVERLAP).strftime("%Y-%m-%dT%H:%M:%SZ"), "limit": 100}
while True:
r = session.get(f"{BASE}/events", params=params)
r.raise_for_status()
page = r.json()
for event in page["data"]:
if seen.get(event["id"]) == event["created_at"]:
continue # the overlap re-reads a few; that is its job
seen[event["id"]] = event["created_at"]
handle(event) # new, or an update of an event you have
created = datetime.fromisoformat(event["created_at"].replace("Z", "+00:00"))
newest = max(newest, created)
if not page["has_more"]:
return newest # store it; it is the next `since`
params["starting_after"] = page["data"][-1]["id"]With curl, the brackets need -g so they are not read as a glob:
curl -G -g https://api.registercheck.de/v1/events \
-H "Authorization: Bearer $REGISTERCHECK_API_KEY" \
-d "created_at[gte]=2026-09-30T14:00:00Z" \
-d "type=company.officer.appointed" \
-d "officer_role=managing_director" \
-d "state=DE-BY" \
-d "limit=100"Three rules:
- Overlap by ten minutes and deduplicate by
idandcreated_at. Delivery is at-least-once: the overlap re-reads events you already have, which is what the deduplication is for. Anidyou have with a newercreated_atis an update: replace what you stored. Keep the last hour or so of pairs; that is enough. - Page forward with
starting_afteruntilhas_moreisfalse. The order is stable (effective_date, thencreated_at, thenid, all descending), so an event that arrives while you page is never served twice in one walk and is caught by the next poll. - Every page costs 10 credits. Poll every few minutes, not every few seconds, and keep
limit=100: a quiet interval is one page. Retrieving one event by id costs 1 credit.
Narrow it
Filters combine with the poll (AND across parameters, OR within a repeated one):
| You want | Add |
|---|---|
| New managing directors | type=company.officer.appointed&officer_role=managing_director |
| Insolvencies and deletions | type=company.insolvency.recorded&type=company.deregistered |
| GmbHs and UGs in Bavaria | legal_form=gmbh&legal_form=ug&state=DE-BY |
| One court | register_court=D2601, or its name (Amtsgericht München) or seat (München) |
| Companies with at least 25,000 EUR capital | capital[gte]=25000 |
| Words in the name, the seat, the court or the entry | q=holding augsburg |
q does not search people's names.
When a cursor stops working
An event whose company or person has been restricted since you listed it is no longer served, and
starting_after naming it answers 400 validation_failed. Do not retry the cursor: run the poll
again from your stored since. That is the same recipe, and it picks up exactly where you were.
What to know
- The window. The list covers the last 180 days of register days. Without any
effective_date[*]orcreated_at[*]bound it shows the last 30. Aneffective_date[gte]older than 180 days answers400 validation_failednaming the parameter. - Completeness. Register days before 2026-09-10 are incomplete.
- Where it comes from. Registercheck reads each court's register continuously; most entries are here on their own register day, from about 07:00.
- Right after a restart the list can answer
503withRetry-After. Wait that many seconds and poll again with the samesince; nothing was charged. - One company, pushed to you. To be told about one company's changes without polling, use a monitor and a webhook.