---
name: ytj-lookup
description: >-
  Look up Finnish companies by name or business ID (Y-tunnus) against a
  read-only API over PRH YTJ open data. Use when the user says "look up a
  Finnish company", "what is the Y-tunnus of X", "is business ID N active",
  "which company form does X have", or needs the registered name, address,
  company form, main business line or registration date of a Finnish Trade
  Register entity. No auth. Backed by the full Trade Register (about 465,000
  companies, refreshed daily from PRH's dump); check /api/v1/dataset for load
  status before treating a 404 as "does not exist".
---

# YTJ Lookup

A read-only HTTP API over Finland's YTJ/PRH Trade Register at
`https://ytj.23signs.org`. Three GET endpoints, JSON in and out, no keys.

## When to use

- The user names a Finnish company and wants its business ID, company form,
  registered address, main business line or registration date.
- The user has a Y-tunnus (format `NNNNNNN-N`, e.g. `0112038-9`) and wants to
  know which company it is and whether it is registered.
- You need a deterministic, keyless company lookup to demonstrate agent tool
  calling against a public API.

Do not use it for toiminimi (sole proprietorships) outside the Trade Register,
or for financial data, beneficial owners or filings (none of that is here).
See Limitations.

## Data

Production reads from Cloudflare D1, loaded from PRH's daily `all_companies`
dump of the Trade Register: 464,751 companies at the time of writing (462,131
registered, 2,620 unregistered or pending). PRH's own paginated API reports
about 827,000 because it also counts historical and removed registrations. The
dump is refreshed around 04:45 UTC and pulled by a crawler once a day. Licence:
Creative Commons BY 4.0, source PRH.

**Load status matters.** The register is being loaded into D1 in daily
budgeted batches (D1 Free plan: 100,000 rows written per day, about 6 rows per
company), so for a while `dataset.complete` is `false` and `dataset.pending`
is greater than zero. While that is the case, a 404 or an empty search can
mean "not loaded yet" rather than "does not exist". Always read the `dataset`
object (or `GET /api/v1/dataset`) before drawing that conclusion, and say so to
the user.

Local dev (`wrangler dev` without `--remote`) has no D1 binding and serves a
bundled sample of 212 companies; `dataset.backend` is then `sample`.

## Setup

Base URL: `https://ytj.23signs.org`. No authentication, no API key, CORS open.

Discovery, if you arrived without this file:

| What | Where |
| --- | --- |
| OpenAPI 3.1 | `{base}/openapi.yaml` (also `.json`) |
| API catalog (RFC 9727 linkset) | `{base}/.well-known/api-catalog` |
| ARD manifest | `{base}/.well-known/ai-catalog.json` |
| llms.txt | `{base}/llms.txt` |

Every `/api/*` response carries a `Link` header with `rel="service-desc"` to
the catalog and the OpenAPI file, and a `_links` object in the body.

## Endpoints

### Dataset status

```
GET {base}/api/v1/dataset
```

Response `200 application/json`:

```json
{
  "backend": "d1",
  "source": "PRH YTJ open data v3 (avoindata.prh.fi), daily all_companies dump",
  "companies": 16200,
  "snapshotDate": "2026-10-05",
  "updatedAt": "2026-10-05T17:14:14+00:00",
  "complete": false,
  "pending": 448551,
  "scope": "Trade Register entities only (Oy, Oyj, Ay, Ky, osuuskunta, housing companies, etc). Toiminimi (sole proprietorships) outside the Trade Register are never included, so a valid-looking Y-tunnus can legitimately return nothing.",
  "_links": { "self": { "href": "https://ytj.23signs.org/api/v1/dataset", "method": "GET", "title": "Dataset status" }, "collection": { "href": "..." }, "openapi": { "href": "..." }, "catalog": { "href": "..." }, "describedby": { "href": "..." } }
}
```

| Field | Meaning |
| --- | --- |
| `backend` | `d1` (production, full register) or `sample` (local dev, 212 companies). |
| `companies` | Companies queryable right now. |
| `snapshotDate` | Date of the PRH dump the data comes from; `null` on the sample. |
| `updatedAt` | Last push to D1, UTC. D1 only. |
| `complete` | `true` once every company in the dump is in D1. |
| `pending` | Companies in the dump not yet pushed. D1 only. |

The same object, without `scope` and `_links`, is embedded as `dataset` in
every search response and in every 404 from the single lookup.

### Search companies

```
GET {base}/api/v1/companies?name=Nokia
GET {base}/api/v1/companies?name=asunto&page=2
GET {base}/api/v1/companies?businessId=0112038-9
```

Parameters (one of `name` or `businessId` is required):

| Name | In | Meaning |
| --- | --- | --- |
| `name` | query | Case-insensitive substring match against the current trade name and current auxiliary names, via an FTS5 trigram index. At least 3 characters. `nokia` matches "Nokia Oyj" and "Nokian Renkaat Oyj". |
| `businessId` | query | Exact Y-tunnus match, `NNNNNNN-N`. |
| `page` | query | Page of name results, 1 to 20, default 1. 25 results per page. |

If both are given, `businessId` wins and `name` is ignored. Within a page,
results are ordered exact match, then prefix matches, then alphabetical.

Response `200 application/json`:

```json
{
  "query": { "businessId": null, "name": "Nokia", "page": 1 },
  "pageSize": 25,
  "hasMore": false,
  "results": [
    {
      "businessId": "0112038-9",
      "name": "Nokia Oyj",
      "auxiliaryNames": ["Nokia Networks", "Nokia Matkapuhelimet"],
      "companyForm": "Public limited company",
      "status": "active",
      "registerStatus": "registered",
      "registrationDate": "1978-03-15",
      "mainBusinessLine": "Activities of head offices",
      "website": "www.nokia.com",
      "address": { "street": "Karakaari 7", "postCode": "02610", "city": "ESPOO" },
      "lastModified": "2026-08-19T10:04:06",
      "_links": { "self": { "href": "https://ytj.23signs.org/api/v1/companies/0112038-9", "method": "GET", "title": "Nokia Oyj" } }
    }
  ],
  "dataset": { "backend": "d1", "source": "...", "companies": 16200, "snapshotDate": "2026-10-05", "updatedAt": "2026-10-05T17:14:14+00:00", "complete": false, "pending": 448551 },
  "note": "Full Trade Register from PRH YTJ open data v3. PRH dump of 2026-10-05. Load in progress: 16200 companies so far, 448551 pending. Trade Register entities only ...",
  "_links": {
    "self": { "href": "https://ytj.23signs.org/api/v1/companies?name=Nokia", "method": "GET", "title": "This search" },
    "collection": { "href": "https://ytj.23signs.org/api/v1/companies", "method": "GET", "title": "Search companies. One of ?name= or ?businessId= is required." },
    "search": { "href": "https://ytj.23signs.org/api/v1/companies{?name,businessId}", "templated": true, "method": "GET" },
    "openapi": { "href": "https://ytj.23signs.org/openapi.yaml", "type": "application/yaml" },
    "catalog": { "href": "https://ytj.23signs.org/.well-known/api-catalog", "type": "application/linkset+json" },
    "describedby": { "href": "https://ytj.23signs.org/SKILL.md", "type": "text/markdown" }
  }
}
```

Pagination: `hasMore: true` means another page exists and `_links.next` holds
its URL; `_links.prev` appears from page 2 on. Follow those links rather than
building URLs. `totalResults` is present for `businessId` searches (0 or 1)
and on the sample backend, but absent for name searches in production: no
count is computed, so that a needle like `asunto` (matching a hundred thousand
housing companies) stays cheap. Pages beyond 20 return 400; narrow the name.

An empty `results` array is a normal 200, not an error. Follow
`results[i]._links.self.href` to fetch one company. Search result items do not
carry `source`.

### Look up one company

```
GET {base}/api/v1/companies/0112038-9
```

Response `200 application/json`: one company object with the same fields as a
search result, plus `source` (`"PRH YTJ open data v3 (avoindata.prh.fi)"`) and
the full `_links` set (`self`, `collection`, `search`, `openapi`, `catalog`,
`describedby`), where `self` is the canonical URL.

## Field notes

| Field | Notes |
| --- | --- |
| `businessId` | Y-tunnus as a string, `NNNNNNN-N`. |
| `name` | Current primary trade name (PRH name type 1 without an end date). |
| `auxiliaryNames` | Current parallel or auxiliary trade names, possibly empty. |
| `companyForm` | English description, e.g. "Public limited company", "Limited company". |
| `status` | `active` when PRH's trade register status code is `1`, otherwise `not_currently_registered`. Two-valued on purpose, for clients that already parse it. |
| `registerStatus` | PRH code list REK_KDI: `unregistered` (0), `registered` (1), `removed_from_register` (2), `startup_not_registered` (3), `ceased` (4), or `unknown` for any other code. |
| `registrationDate` | `YYYY-MM-DD` or null. |
| `mainBusinessLine` | English TOL 2008 description, or null. |
| `address` | Primary (visiting) address: `street`, `postCode`, `city`. Any part may be null. |
| `lastModified` | Upstream modification timestamp, or null. |
| `source` | Single lookups only: `"PRH YTJ open data v3 (avoindata.prh.fi)"`. |

## Errors

All errors are RFC 9457 `application/problem+json` with `type`, `title`,
`status`, `detail`, `instance`, and recovery extensions.

Missing parameters:

```json
{
  "type": "https://ytj.23signs.org/errors/BAD_REQUEST",
  "title": "BAD_REQUEST",
  "status": 400,
  "detail": "Provide at least one of: businessId, name",
  "instance": "/api/v1/companies",
  "example": "https://ytj.23signs.org/api/v1/companies?name=Nokia",
  "_links": { "collection": { "href": "..." }, "openapi": { "href": "..." }, "catalog": { "href": "..." } }
}
```

Name too short (`?name=ab`) returns the same shape with
`"detail": "name must be at least 3 characters"`; `page` outside 1..20 returns
`"detail": "page must be between 1 and 20; narrow the name instead"`.

Unknown business ID while the load is still in progress:

```json
{
  "type": "https://ytj.23signs.org/errors/NOT_FOUND",
  "title": "NOT_FOUND",
  "status": 404,
  "detail": "No company with businessId 0000000-0. The register is still loading (16200 of the register so far), so the ID may simply not be in yet; it can also be a toiminimi outside the Trade Register.",
  "instance": "/api/v1/companies/0000000-0",
  "availableEndpoints": ["GET /api/v1/companies", "GET /api/v1/companies/{businessId}", "GET /api/v1/dataset"],
  "dataset": { "backend": "d1", "companies": 16200, "complete": false, "pending": 448551, "snapshotDate": "2026-10-05", "updatedAt": "...", "source": "..." },
  "_links": { "collection": { "href": "..." }, "search-by-name": { "href": "https://ytj.23signs.org/api/v1/companies?name=" } }
}
```

Once `dataset.complete` is `true` the `detail` reads instead: "The ID is not in
the Trade Register: it may be a toiminimi (sole proprietorship) outside the
register, or not exist." Branch on `dataset.complete`, not on the prose.

Unknown route under `/api/` returns the same shape with `availableEndpoints`.

## Limitations

- **Load in progress.** Until `dataset.complete` is `true`, a miss is
  inconclusive. Report it as "not found in the currently loaded part of the
  register (N of about 465,000 companies)" and point at `/api/v1/dataset`.
- **No toiminimi.** PRH's Trade Register data covers Oy, Oyj, Ay, Ky,
  osuuskunta, housing companies and similar. Sole proprietorships that are not
  in the Trade Register are never returned, even with a valid Y-tunnus.
- **Daily snapshot.** The data is as fresh as the last PRH dump
  (`dataset.snapshotDate`), refreshed around 04:45 UTC and pushed once a day.
  Registrations made today will not be visible yet.
- **No result counts for name searches.** Use `hasMore` and `_links.next`.
  Names shorter than 3 characters are refused.
- **English only.** Descriptions are the English variant where PRH provides
  one; the Finnish and Swedish strings are not exposed.
- **Read-only.** No writes, no filtering beyond name and business ID.

## Worked examples

Check what is loaded before anything else:

```sh
curl -s https://ytj.23signs.org/api/v1/dataset \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["backend"], d["companies"], "complete" if d["complete"] else str(d.get("pending"))+" pending")'
# d1 16200 448551 pending
```

Business ID of a company by name:

```sh
curl -s "https://ytj.23signs.org/api/v1/companies?name=Fortum" \
  | python3 -c 'import sys,json;[print(r["businessId"], r["name"], r["registerStatus"]) for r in json.load(sys.stdin)["results"]]'
# one line per match, e.g. 1463611-4 Fortum Oyj registered
```

Is a Y-tunnus active, and which company is it?

```sh
curl -s https://ytj.23signs.org/api/v1/companies/0112038-9 \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("name"), d.get("status"), d.get("companyForm"), d.get("detail"))'
# Nokia Oyj active Public limited company None     (when loaded)
# None None None No company with businessId ...    (404 while that record is still pending)
```

Walk pages of a broad search:

```sh
url="https://ytj.23signs.org/api/v1/companies?name=asunto"
while [ -n "$url" ]; do
  url=$(curl -s "$url" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d["results"]), "results, page", d["query"]["page"], file=sys.stderr);print(d["_links"].get("next",{}).get("href",""))')
done
```

Handle a miss:

```sh
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
  https://ytj.23signs.org/api/v1/companies/0000000-0
# 404 application/problem+json
```

Well-known business IDs to try (present once the load reaches them):
`0112038-9` (Nokia Oyj), `1463611-4` (Fortum Oyj), `1039050-8` (Stora Enso
Oyj), `0116510-6` (Elisa Oyj), `0680006-8` (Nokian Renkaat Oyj), `0108023-3`
(Finnair Oyj).

## Gotchas

- Business IDs need the hyphen: `0112038-9`, not `01120389`.
- Query values are URL-encoded as usual; a name with spaces or `&` must be
  encoded.
- `Accept: application/json` on `/` redirects (303) to the API catalog;
  `Accept: text/markdown` on `/` returns llms.txt.
- Responses are cacheable (`Cache-Control: public, max-age=300` on the API,
  one hour on discovery documents). Data changes once a day after the crawler
  runs, and more often while the initial load is in progress.
