Metadata-Version: 2.4
Name: yaybo
Version: 1.2.0
Summary: Fetch, explore and export the Danish property registers, from a terminal
Keywords: tinglysning,denmark,property,land-registry,tui,mitid
Author: Kilian Tscherny
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Natural Language :: Danish
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database :: Front-Ends
Classifier: Topic :: Office/Business :: Financial
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: duckdb>=1.5.5
Requires-Dist: mitid-client[textual]>=0.1.1
Requires-Dist: openpyxl>=3.1
Requires-Dist: requests>=2.32
Requires-Dist: textual>=3.1
Requires-Dist: textual-plotext>=1.0
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/kiliantscherny/yaybo
Project-URL: Repository, https://github.com/kiliantscherny/yaybo
Project-URL: Changelog, https://github.com/kiliantscherny/yaybo/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/kiliantscherny/yaybo/issues
Description-Content-Type: text/markdown

<p align="center">
  <img src="static/yaybo-logo.png" alt="yaybo" width="200" />
</p>

<h1 align="center">yaybo</h1>

<p align="center">
  Look up a Danish address, see what the land register (Tingbogen) holds on it, and keep the results in a local DuckDB database you can browse, query and export from your terminal.
  <br>
  <a href="https://pypi.org/project/yaybo/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/yaybo?style=flat&logo=python&logoColor=orange&label=yaybo&labelColor=teal&color=navy"></a>
</p>

<p align="center">
<img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776AB?logo=python&logoColor=white" alt="Python versions" />
<a href="https://github.com/j178/prek"><img src="https://img.shields.io/badge/prek-enabled-brightgreen?logo=pre-commit&logoColor=white" alt="prek" style="max-width:100%;"></a>
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv" style="max-width:100%;"></a>
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty" style="max-width:100%;"></a>
<a href="https://github.com/tox-dev/tox-uv"><img src="https://img.shields.io/badge/tox-testing-1C1C1C?logo=tox&logoColor=white" alt="tox" style="max-width:100%;"></a>
<a href="https://github.com/kiliantscherny/yaybo/actions/workflows/ci.yml"><img src="https://github.com/kiliantscherny/yaybo/actions/workflows/ci.yml/badge.svg" alt="CI" style="max-width:100%;"></a>
<a href="https://github.com/kiliantscherny/yaybo/actions/workflows/release.yml"><img src="https://github.com/kiliantscherny/yaybo/actions/workflows/release.yml/badge.svg" alt="Release to PyPI" style="max-width:100%;"></a>
</p>

---

> [!CAUTION]
> **This is a hobby project. It is not built for production use, and nothing
> about it is supported.**
>
> It is not affiliated with, endorsed by, or connected to tinglysning.dk,
> Domstolsstyrelsen, MitID, NemLog-in, Danmarks Statistik or
> Dataforsyningen. Those names appear here only to say where the data comes
> from.
>
> Provided as-is, with no warranty of any kind. **Use it at your own risk.** The
> author accepts no liability for any loss, damage, or misuse arising from it,
> and none of it is financial, legal or property advice.
>
> It fetches records about real, named people. What that obliges you to is in
> [Legal and data protection](#legal-and-data-protection); read it before you
> fetch anything you did not come here for.

## Install

```sh
uv tool install yaybo    # or: pip install yaybo
```

Python 3.10 or newer. `uvx yaybo` runs it without installing anything.

## Quickstart

```sh
yaybo                                       # open the TUI
yaybo fetch "Prøvegade 1, 9999 Prøveby"     # fetch one address
yaybo --version                             # which version is installed
```

Results go to `out/tinglysning.duckdb`. Looking the same address up again replaces its rows rather than adding a second copy.

## Where the data comes from

| source | what it provides | login needed |
| --- | --- | --- |
| **tinglysning.dk** | owners, mortgages and charges, easements, past transfers, sale history | partly |
| **Danmarks Statistik** | what each kind of realkredit loan cost month by month, used to read a bare interest rate as an F3 or a fixed loan | no |
| **DAWA** | address lookup and validation while you type, and the official coordinates | no |
| **BBR** (via Datafordeleren) | the building record — year built, rooms, walls, heating — and the living area a listing quotes | free API key |

Most of it is available without logging in. Logging in with MitID adds owners' dates of birth, everyone named on each mortgage, and the history of previous owners.

All four are public registers or open government APIs — see [Legal and data protection](#legal-and-data-protection). Sale history comes from the register itself, so it needs a login; the BBR record comes from Datafordeleren, which needs a free key — see [The BBR record](#the-bbr-record).

> [!NOTE]
> This tells you who **owns** a property, not who lives there. Resident data
> (CPR/folkeregisteret) is not public in Denmark, with or without a login. If a
> property is rented out you get nothing about the tenant. The owner is
> sometimes a company with a CVR number rather than a person.

## The TUI

> [!WARNING]
> The TUI is a work in progress and will change.

Run `yaybo` with no arguments. Eight screens, all reading the same database:

| screen | key | what it is for |
| --- | --- | --- |
| **Properties** | `l` | everything you have fetched, searchable offline |
| **Co-op shares** | tab | what you have fetched from the other register |
| **Buildings** | `g` | the properties grouped one building to a row |
| **Search** | `/` | find an address, see what the registers hold at it |
| **Queue** | `b` | fetch many properties in the background |
| **Figures** | `k` | across a set of properties rather than about one |
| **SQL** | `s` | query the database directly |
| **Property** | `enter` | one property in full |

**Properties** is where it opens. Each row shows how stale it is, its valuation, debt and loan-to-value, and whether it was fetched while logged in. Sort by any column with `o`, filter with `name:value`, tick rows with `space` and act on the lot. `enter` opens a property, `f` re-fetches, `e` exports.

**Search** resolves your typing against [DAWA](https://dawadocs.dataforsyningen.dk/), then asks the register which properties actually sit at that address – one for a rented block, over a hundred for a block of owner-occupied flats. Tick the ones you want and press `f` to hand them to the queue.

**Property** shows one property, tab by tab, read back out of the database, so it is instant and works offline:

- **Oversigt** – what it is, who owns it, what it is worth, what it owes
- **Ejere · Hæftelser · Servitutter · Parter · Handler** – the tables in full, with each charge's interest terms and the loan product it implies
- **Forløb** – sales, transfers, mortgages, easements and valuations on one timeline. The register keeps these as four separate lists
- **Kurve** – price per square metre over time
- **Bygning** – the BBR record

The signed attest has no tab of its own. It is stored, exported and queryable — `attester.dokument` is the document as signed and `dokument_json` the same thing for `json_extract` — but it is a few hundred kilobytes of OIO XML, and everything worth having out of it is already the tables above.

**Co-op shares** is the same idea for the andelsboligbog, which is a different register about a different thing – see [Andelsboliger](#andelsboliger). Each row is one co-op share: its area, what is charged against it, and the association's building. `enter` opens a share, with its charges and everyone named on them and any notices; `g` opens the building, which is where a share's valuation and the association's own mortgages live.

**Buildings** groups the properties one building to a row: how many of its properties you hold, how many were fetched while logged in, and the medians across them. `enter` goes to that building's properties.

**Queue** takes what Search hands it and fetches in the background, with a progress bar and per-row status. Pause with `space`. Anything already fetched is already saved, so a lapsed login partway through costs you nothing.

**Figures** answers questions about a set of properties rather than one: median price per m² by floor, valuations by building, owners by postcode. Narrow the set with dropdowns filled from the database, then pick a figure. Nothing is fetched – it describes only the properties you already hold.

**SQL** runs read-only queries against the whole database, with eight examples ready to load and edit. `ctrl+R` runs, `ctrl+E` exports the result.

Press `ctrl+L` anywhere to log in with MitID, or to log out, and `ctrl+G` to switch language.

### English or Danish

The interface comes in both and opens in English. `ctrl+G` switches at any point and rebuilds every screen in the chosen language, putting you back where you were; the choice is remembered for next time.

**The data is never translated.** A register record is Danish — `Ejerpantebrev`, `Almindeligt salg`, the wording of an easement — and reading one is a Danish-language job however the buttons around it are labelled. Anglicising a value would also make the database disagree with the register it came from, so only the chrome moves: headings, column labels, tabs, footer keys and the sentences a screen writes about what it is showing.

The command line and the exports are English throughout and are not affected by the setting.

## On the command line

```sh
yaybo                              # the TUI
yaybo --version                    # the installed version
yaybo fetch ADDRESS [ADDRESS ...]  # look addresses up and store them
yaybo export                       # write what is stored to a spreadsheet
yaybo login --user YourMitIDUserID
yaybo status                       # is the session good, and for how long
yaybo keepalive [MINUTES]          # hold it open without another trip to the phone
yaybo backfill                     # re-derive stored tables, fetching nothing
yaybo logout
```

`fetch` takes several addresses at once and pauses between them. One that cannot be resolved is reported and skipped; the rest still get fetched.

```sh
yaybo fetch "Prøvegade 1, 9999 Prøveby" "Prøvevej 2, 9999 Prøveby"
```

Useful `fetch` options:

| option | what it does |
| --- | --- |
| `--format LIST` | `duckdb` (default), `csv`, `xlsx`, or a comma-separated list |
| `--limit N` | most properties to fetch (default 25, `0` for no limit) |
| `--anonymous` | ignore any cached session and use only the public lookup |
| `--delay SECONDS` | pause between fetches (default 1.0) |
| `--outdir DIR` | where results go (default `out/`, which is git-ignored) |
| `--no-andele` | skip the andelsboligbog; co-op blocks give only the association's property |
| `--no-bbr` | skip BBR; no building record, and no price per m² over the living area |
| `--no-laantype` | skip estimating each charge's loan type |
| `--keepalive [MIN]` | hold the session open afterwards (default 60) |

`--debug` and `--db PATH` work before or after the subcommand.

`export` writes what is already stored, rather than what was just fetched:

```sh
yaybo export                                    # everything -> exports/*.xlsx
yaybo export --format csv --name priser --query "SELECT ..."
yaybo export --query-file report.sql --name rapport
```

It prints the written path on stdout and nothing else, so `file=$(yaybo export)` gives you the file.

`fetch` and the TUI share the same pipeline, so they cannot drift apart.

`backfill` rebuilds every table derived from a stored document – charges, easements, the people named on them, previous owners – with no login and no requests to the register. Run it after upgrading, when a reader has improved.

## The data

One DuckDB file, fifteen tables. Twelve of them are keyed on the property; the other three are the andelsboligbog, which is a different register about a different thing – see [Andelsboliger](#andelsboliger) below.

```mermaid
erDiagram
    ejendomme ||--o{ ejere : owns
    ejendomme ||--o{ haeftelser : "charged with"
    ejendomme ||--o{ servitutter : "burdened by"
    ejendomme ||--o{ bygninger : "built on"
    ejendomme ||--o{ handelshistorik : "sold as"
    ejendomme ||--o{ adkomsthistorik : "transferred as"
    ejendomme ||--o| attester : "documented by"
    ejendomme ||--o{ dokument_parter : "named on"
    haeftelser ||--o{ underpant : "pledged as"
    haeftelser ||--o{ dokument_parter : names
    servitutter ||--o{ dokument_parter : names
    adkomsthistorik ||--o{ adkomsthistorik_ejere : names
    ejendomme ||--o{ andele : "shares in"
    andele ||--o{ andel_haeftelser : "charged with"
    andele ||--o{ andel_meddelelser : "noted on"

    ejendomme {
        varchar uuid PK
        varchar adresse
        bigint ejendomsvurdering_dkk
        bigint samlet_gaeld_dkk "derived"
        bigint frivaerdi_dkk "derived"
        double belaaningsgrad_pct "derived"
        boolean beriget "fetched while logged in"
    }
    ejere {
        varchar ejendom_uuid PK, FK
        bigint nummer PK
        varchar navn
        date foedselsdato "login only"
        varchar cvr
    }
    haeftelser {
        varchar ejendom_uuid PK, FK
        varchar dokument_uuid PK
        varchar dokument_version PK
        bigint prioritet
        bigint hovedstol_dkk
        double rentesats_pct
        varchar laantype_estimat "estimated"
    }
    servitutter {
        varchar ejendom_uuid PK, FK
        varchar dokument_uuid PK
        varchar dokument_version PK
        varchar dokumenttype
        varchar tekst
    }
    dokument_parter {
        varchar ejendom_uuid PK, FK
        varchar dokument_uuid PK, FK
        varchar dokumentart PK
        varchar rolle PK
        bigint nummer PK
        varchar navn
        date foedselsdato "login only"
    }
    underpant {
        varchar ejendom_uuid PK, FK
        varchar haeftelse_uuid PK, FK
        varchar rettighed_uuid PK
        bigint beloeb_dkk
    }
    handelshistorik {
        varchar ejendom_uuid PK, FK
        varchar registrering_id PK
        date dato
        bigint beloeb_dkk
        bigint pris_pr_m2
    }
    bygninger {
        varchar ejendom_uuid PK, FK
        varchar bygning_nr PK
        bigint opfoerelsesaar
        bigint boligareal_m2
        varchar varmeinstallation
    }
    adkomsthistorik {
        varchar ejendom_uuid PK, FK
        bigint post_nummer PK
        date dato
        bigint koebesum_dkk
    }
    adkomsthistorik_ejere {
        varchar ejendom_uuid PK, FK
        bigint post_nummer PK, FK
        bigint nummer PK
        varchar navn
    }
    attester {
        varchar ejendom_uuid PK, FK
        varchar dokument "as signed"
        json dokument_json "queryable"
    }
    rentestatistik {
        varchar maaned PK
        varchar rentfix_kode PK
        varchar laantype
        double effektiv_rente_pct
    }
    andele {
        varchar uuid PK "andelsboligbogen"
        varchar adresse
        varchar ejendom_uuid FK "the association's building"
        bigint boligareal_m2 "wants BBR; the book records none"
        bigint samlet_gaeld_dkk "derived: this share only"
    }
    andel_haeftelser {
        varchar andel_uuid PK, FK
        varchar dokument_uuid PK
        varchar dokument_version PK
        varchar dokumenttype
        bigint hovedstol_dkk
    }
    andel_meddelelser {
        varchar andel_uuid PK, FK
        varchar dato_loebenummer PK
        varchar dokumenttype
        varchar debitorer "the andelshaver"
        varchar disponenter "who may act for them"
    }
```

| table | one row per | needs login |
| --- | --- | --- |
| `ejendomme` | property, with valuation, debt and equity | no |
| `ejere` | current owner | no |
| `haeftelser` | mortgage or charge, with interest terms and estimated loan type | no |
| `servitutter` | easement, and what it is about | no |
| `dokument_parter` | person named on a document, with their role and date of birth or CVR | **yes** |
| `underpant` | deed pledged on in its own right | no |
| `handelshistorik` | recorded sale, with price per m² | no |
| `bygninger` | building in the BBR record | no |
| `adkomsthistorik` | past transfer, with what was paid | **yes** |
| `adkomsthistorik_ejere` | person named in one of those transfers | **yes** |
| `attester` | the property's whole register document, signed and as JSON | **yes** |
| `rentestatistik` | month of DST realkredit rates | no |
| `andele` | co-op share, from the andelsboligbog | no |
| `andel_haeftelser` | charge registered against one share | no |
| `andel_meddelelser` | notice noted on a share: death, bankruptcy, seizure | no |

`rentestatistik` is not about any one property. It is the rate series `laantype_estimat` was matched against, kept so an estimate can be checked.

The full column-level schema is in [schema.dbml](schema.dbml), generated from the code so it cannot fall behind it. Paste it into [dbdiagram.io](https://dbdiagram.io) for a browsable diagram.

### Andelsboliger

Tinglysning is four registers, not one, and two of them matter here. The **tingbog** records real property. The **andelsboligbog** records shares in housing associations, and they disagree about what a co-op building is — correctly, in both cases:

- To the tingbog, a co-op block is **one property**, owned by the association, however many doors it has. That is the row in `ejendomme`.
- To the andelsboligbog, the same block is **one entry per flat**. Those are the rows in `andele`.

Both are fetched, and `andele.ejendom_uuid` joins the second to the first. A lookup that finds shares says so: *found 1 property and 10 co-op shares*.

What the second book actually holds is much less than the first. A share is not land, so it has **no valuation, no matrikel, no registered area and no easements** — only its address, whatever is charged against it, and any notices. The area on an `andele` row comes from BBR rather than the register, and the coordinates from DAWA.

**There is no owner of record.** The andelsboligbog registers rights *over* a share, not title *to* one; who holds an andel is the association's record, not the register's. Two places name people anyway:

- `andel_haeftelser.kreditorer`. Most charges on a share are an **ejerpantebrev** — a deed the owner issues to *themselves* and then pledges to a bank — so its creditor is in practice the andelshaver. That is an inference from the instrument, not something the register states.
- `andel_meddelelser.debitorer` and `.disponenter`. A notice is the register recording that something has happened to the andelshaver rather than to the flat: a death, a bankruptcy, a court removing their power to dispose of it. It names them, and whoever may now act for them.

Neither carries a date of birth. Those come from the CPR numbers printed on a signed attest, and no attest for a share has been read here — see below.

Three things worth knowing before querying it:

- **A flat missing from `andele` is not evidence it is not an andel.** A share only enters the book once something is registered against it.
- **`andele.samlet_gaeld_dkk` is not what living there owes.** It totals what is charged against that share alone. An andelshaver also owes their portion of the association's own mortgage, which is registered against the *building* in the tingbog and is nowhere in this table.
- **There is no sale price for a share, and there is no table of them.** A share is not sold as real property, so the register records no transfer for one — the book holds rights *over* a share, not title *to* it. Any sale at a co-op address is the building's own, which is a fact about the building and is stored as one on the `ejendomme` row.

One thing is known to exist and is **not** read here: a logged-in session can fetch a signed andelsboligbogsattest (`rest/andelsbolig/...`) and search the book by person name and date of birth. By analogy with the tingbog that attest would carry parties' CPR-derived birth dates. It is unverified and nothing here depends on it.

In the TUI, **Co-op shares** lists the shares held; enter opens one - its charges, everyone named on them, and its notices - and `g` opens the association's property.

`yaybo fetch --no-andele` skips the second book and behaves as earlier versions did. `yaybo backfill` cannot rebuild these two tables — the register stores no signed document for a share, so there is nothing to re-derive them from — and leaves them untouched.

Every table has a primary key, so a row is identifiable and a re-run replaces rather than duplicates. Relationships are drawn above but not enforced: DuckDB cannot add a foreign key to an existing table, so enforcing them would leave every database created before this version permanently unable to catch up.

> [!WARNING]
> **Some columns are worked out, not recorded, and they can be wrong.**
>
> `samlet_gaeld_dkk`, `frivaerdi_dkk` and `belaaningsgrad_pct` are derived, and
> they run against the **public valuation**, which sits well below market. Treat
> the equity as a floor and the loan-to-value as a ceiling.
>
> `laantype_estimat` is an estimate, not a record. The register gives an
> interest rate and never the product, so this is that rate matched against what
> each kind of loan cost in the months around it. `laantype_afstand` holds the
> distance to the runner-up, and `rentestatistik` holds the whole series, so the
> estimate can be argued with.

## Exporting

Press `e` on any screen, pass `--format` to `fetch`, or run `yaybo export` against what is already stored.

| format | shape |
| --- | --- |
| **DuckDB** | one table each, as they already are |
| **Excel** | one sheet per table, header row frozen |
| **CSV** | one file per table, in a folder named after the address |

Column order follows the schema, so the same table exported twice has the same columns in the same places.

## Using it with an AI agent

The TUI is for people. Agents should use the CLI, which is non-interactive apart from the login.

### Install the plugin (no clone needed)

In [Claude Code](https://claude.com/claude-code) – terminal or desktop – type:

```
/plugin marketplace add kiliantscherny/yaybo
/plugin install yaybo@yaybo
```

That is the whole setup. It works from any folder, and gives the agent the CLI reference, the data model and the schema. The only thing you need on your machine is [uv](https://docs.astral.sh/uv/) – it installs Python itself, so that is not a separate prerequisite:

```sh
curl -LsSf https://astral.sh/uv/install.sh | sh          # macOS and Linux
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"   # Windows
```

Then just ask, in plain language:

> What does the land register hold on Prøvegade 1 and Prøvevej 2 in 9999
> Prøveby? Put it in a spreadsheet.

The agent fetches with `yaybo fetch`, queries the database, and hands back an Excel file. Fetched data lands in `out/` in whatever folder you are working in, so it is worth making one and staying in it:

```sh
mkdir ~/property-lookups && cd ~/property-lookups
```

### Or clone the repository

Cloning gets you the source. To use the skill from a clone without installing it, point Claude Code at the plugin directory:

```sh
git clone https://github.com/kiliantscherny/yaybo.git
cd yaybo
claude --plugin-dir ./plugin
```

Either way the agent has:

| file | what it gives the agent |
| --- | --- |
| [AGENTS.md](AGENTS.md) | the project, its rules, and what not to do (`CLAUDE.md` symlinks to it) |
| [schema.dbml](schema.dbml) | every table, column, type and key, generated from the code |
| `plugin/skills/danish-property-records/` | the CLI, the data model and worked queries, loaded only when needed |

> [!IMPORTANT]
> **The MitID login cannot be automated, by design.** It needs a person to
> approve a push notification in the MitID app or scan a QR code. An agent that
> runs `yaybo login` in a background shell will simply hang.
>
> Most data needs no login at all. If you want owners' dates of birth, everyone
> named on a mortgage, or previous owners, run the login yourself – in Claude
> Code, type `! yaybo login --user YourMitIDUserID` – and let the agent carry on
> afterwards. `yaybo status` says whether a session is live.

> [!NOTE]
> If you only want to look a property up, you do not need any of this.
> `uvx yaybo` opens the TUI and that is the whole thing. An agent is worth it
> when you want several properties compared, or a spreadsheet at the end.

## What you are taking on

> [!WARNING]
> **Everything this fetches is about real, named people** – what they paid for
> their home, what they still owe on it, and, once logged in, when they were
> born. It is public record, which is not the same as being yours to do
> anything with.
>
> - Once you fetch it, you are holding it. In the EU that comes with
>   obligations, and "it was already public" does not answer for what you do
>   next.
> - `out/` and `exports/` are git-ignored on purpose, as is every data file
>   anywhere in the tree. Keep it that way.
> - Look up addresses you have a reason to look at.

> [!IMPORTANT]
> The registers are public services, not scraping targets. There is a pause
> between requests, no attempt to go faster than a person clicking, and the
> queue is rate-limited for the same reason. Please leave it that way.

Logging in means logging in as you, to a government register, with MitID.

## Legal and data protection

None of this is legal advice. It is where the project understands itself to stand, and what that leaves to you.

### The register is public, with two conditions attached

Anyone may look up any property in the tingbog – that is what a public register is for. Two rules govern what happens next, and both are about **disclosure** rather than access:

- **`tinglysningsloven` § 50 c, stk. 1** – *"Oplysninger i edb-registrene om personnumre må ikke videregives."* CPR numbers may not be passed on.
- **[Domstolsstyrelsen's own guidance](https://www.domstol.dk/tinglysningsretten/offentlighed/)** – data from Den Digitale Tingbog must not be stored *with a view to passing it to third parties*. Storing it for the internal use of whoever retrieved it is fine. If somebody else wants it, send them to the Tingbog.

That is the shape this is built to: a database file on the machine of the person who logged in, publishing nothing and serving nothing. What is distributed here is a **tool**, not a **database**, and that difference is most of the answer.

**A CPR number is never stored.** The attest prints `Cpr-nr.: 010195-****`, and only the first six digits – the birth date – are read out of it. `register/fields.py` drops the serial deliberately, on the reasoning that it should stay dropped even on the day the register stops masking it. `foedselsdato` is that birth date; no column anywhere holds a personnummer.

> [!CAUTION]
> **The signed attests are the sensitive thing here.** `attester.dokument` is
> the register's own document as signed, and `yaybo fetch --format csv` or
> `--format xlsx` writes each one out as its own file. It is a complete,
> authenticated statement of a named person's position – the one output where
> passing it on is squarely the thing the rules above prohibit. Leave it where
> it lands.

### Every source here is a public one

All four are public registers or open government APIs, published to be read by programs: [DAWA](https://dawadocs.dataforsyningen.dk/) and Danmarks Statistik are documented open APIs, BBR is distributed by Datafordeleren, and the land register is public by statute.

That is a rule rather than a coincidence, and it is what a new source has to clear. A private site's terms and its `robots.txt` are checked before a line is written against it, and an open endpoint is not the same as permission to use it — an API that answers without a key may still be one its owner has asked robots to leave alone.

It is also why BBR is behind a credential here rather than scraped from somewhere easier. Every official distribution of BBR is gated: Datafordeleren's REST and GraphQL both refuse an anonymous request, BBR's own map component disallows robots outright, and OIS disallows the endpoints that serve a BBR-meddelelse. There is no permitted keyless route, so this asks for a key instead of going looking for a gap.

Two consequences worth knowing when reading the data:

- **Sale history comes from the register's own *historisk adkomst***, which is where the rest of the country's sale prices originate anyway — Vurderingsstyrelsen takes its price data from tinglysning. It **needs a login**.
- **There are two prices per m², because there are two areas.** `pris_pr_m2` divides by the BBR living area, which is what a listing quotes; `pris_pr_m2_tinglyst` divides by the register's own tinglyste areal. They are not the same number and can differ by a third. Each is empty when its own area is, rather than falling back to the other.

### The BBR record

The land register says nothing about the building itself — no year of construction, no rooms, no heating, and no living area. That is BBR's job, and BBR is distributed by **Datafordeleren**, which needs a credential.

It is free and it is optional. Create an account on [Datafordeler Administration](https://datafordeler.dk/) with an email address, generate an API key, and put it where yaybo will find it:

```sh
export DATAFORDELER_API_KEY=...        # or a .env file in the folder you work in
```

Without a key everything else fills exactly as it does now — owners, charges, easements, valuation, debt, equity, sale history — and only the BBR columns stay empty. `uvx yaybo` has to keep working for someone who has never heard of Datafordeleren, so nothing here depends on it.

Two things worth knowing, both of which cost an afternoon to find out:

- The key works on **GraphQL only**. Datafordeleren's REST services refuse it, and REST is being retired at the end of 2026 anyway.
- A new key is **not live for about 15 minutes**. Until then the service answers `401 DAF-AUTH-0005`, which reads like a wrong key and is not.

Why a key at all, when BBR is public data? Because every official distribution of it is behind either a credential or a robots rule — Datafordeleren's REST and GraphQL both refuse an anonymous request, BBR's own map component disallows robots outright, and OIS disallows the endpoints that serve a BBR-meddelelse. There is no keyless route, so this asks for a key rather than going looking for a gap.

### What GDPR asks of you

Article 2(2)(c) exempts processing "by a natural person in the course of a purely personal or household activity", and looking up the flat you are about to buy plausibly sits inside it. The exemption is read narrowly, and two things leave it behind:

- Accumulating many named people's finances stops looking like a household activity, however local the file stays.
- Using it for work – as an agent, a lender, a journalist, a researcher – leaves the exemption altogether, and makes you a controller with everything that carries.

The code does not decide that. It is a local database, and what it is for is a question about you rather than about it.

## mitid-client

The MitID login is a separate library: [mitid-client](https://github.com/kiliantscherny/mitid-client). It knows nothing about property – it is a Python stand-in for MitID's JavaScript core client, the NemLog-in broker that fronts the Danish public sector, a store for keeping a login's cookies between runs, and two ways of showing a login to whoever is doing it: a few lines on stderr, or a Textual screen.

```python
from mitid.brokers import nemlogin
from mitid.ui.tui import MitIDLoginScreen

session = nemlogin.new_session()
result = await self.push_screen_wait(
    MitIDLoginScreen(partial(nemlogin.log_in, session, START_URL))
)
```

Point it at any NemLog-in-protected URL and it returns the session cookie that URL was guarding. It installs as a dependency of this, so there is nothing to do about it. It is worth knowing about separately because the login is the reusable half.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Releases are described in [RELEASING.md](RELEASING.md), and changes in [CHANGELOG.md](CHANGELOG.md).

MIT licensed.
