{
  "openapi": "3.1.0",
  "info": {
    "title": "YTJ Lookup API",
    "version": "0.2.0",
    "summary": "Read-only lookup of Finnish companies by name or business ID, from PRH YTJ open data.",
    "description": "A friendlier wrapper around Finland's YTJ/PRH Trade Register, intended for both human search and AI agent tool-calling. No authentication.\nData: the full Trade Register from PRH's YTJ open data v3, loaded from the daily `all_companies` dump (about 465,000 companies; the dump is refreshed around 04:45 UTC) into Cloudflare D1. PRH's paginated API reports about 827,000 entities because it also counts historical and removed registrations; the dump carries the current register. Licence: Creative Commons BY 4.0, source PRH.\nLoading status: the register is loaded in daily budgeted batches, so for a while `dataset.complete` is false and `dataset.pending` is greater than zero. Check `GET /api/v1/dataset`, or the `dataset` object that every search response and every 404 carries, before concluding that a missing business ID does not exist.\nKnown limitation (inherited from the upstream PRH data): this only covers Trade Register entities (Oy, Oyj, Ay, Ky, osuuskunta, housing companies and similar). It never covers toiminimi (sole proprietorships) that are not registered in the Trade Register.\nEvery JSON response carries a `_links` object (self, collection, search, openapi, catalog, describedby) and every `/api/*` response carries a `Link` header with rel=\"service-desc\" to the RFC 9727 catalog and this document. Errors are RFC 9457 application/problem+json.\nAgent discovery: /.well-known/api-catalog (RFC 9727), /.well-known/ai-catalog.json and /.well-known/ard.json (ARD manifest), /.well-known/mcp/server-card.json, /.well-known/agent-card.json, /.well-known/agent-skills/index.json, /llms.txt, /SKILL.md.\n",
    "contact": {
      "name": "23signs.org",
      "url": "https://23signs.org"
    },
    "license": {
      "name": "Data licence CC BY 4.0 (source PRH)",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    }
  },
  "externalDocs": {
    "description": "Agent skill file (when to use, parameters, shapes, limitations, examples)",
    "url": "https://ytj.23signs.org/SKILL.md"
  },
  "servers": [
    {
      "url": "https://ytj.23signs.org",
      "description": "Production (Cloudflare Worker, D1 backend with the full Trade Register)"
    },
    {
      "url": "http://localhost:8787",
      "description": "Local dev (wrangler dev; serves the bundled 212-company sample unless run with --remote)"
    }
  ],
  "tags": [
    {
      "name": "companies",
      "description": "Finnish Trade Register companies (PRH YTJ open data v3)"
    },
    {
      "name": "dataset",
      "description": "What is behind the API right now (backend, snapshot date, load progress)"
    }
  ],
  "paths": {
    "/api/v1/dataset": {
      "get": {
        "operationId": "getDataset",
        "tags": [
          "dataset"
        ],
        "summary": "Dataset status",
        "description": "Which backend answers (`d1` in production, `sample` in local dev), how many companies it holds, the PRH dump date, when it was last pushed, and whether the load is complete. The same object (without `scope` and `_links`) is embedded as `dataset` in every search response and every 404 on a single lookup.\n",
        "responses": {
          "200": {
            "description": "Dataset status",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatasetResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/companies": {
      "get": {
        "operationId": "searchCompanies",
        "tags": [
          "companies"
        ],
        "summary": "Search companies by name or business ID",
        "description": "Provide at least one of `businessId` or `name`. `name` does a case-insensitive substring match against the current trade name and current auxiliary names (FTS5 trigram index), so it must be at least 3 characters. If both are given, `businessId` takes precedence.\nResults come in pages of 25. Inside a page the order is exact match first, then prefix matches, then alphabetical. `hasMore` says whether another page exists; follow `_links.next` rather than computing URLs. `totalResults` is present for `businessId` searches and on the sample backend; it is absent for name searches on the production (D1) backend because no count is computed, to bound the rows read.\nZero matches is a normal 200 with an empty `results` array. Before treating an empty result as \"no such company\", read `dataset`: while `dataset.complete` is false the register is still loading.\n",
        "parameters": [
          {
            "name": "businessId",
            "in": "query",
            "description": "Finnish business ID (Y-tunnus), format NNNNNNN-N. Exact match.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{7}-\\d$",
              "example": "0112038-9"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Company name or part of it, at least 3 characters. Case-insensitive substring over current and auxiliary names.\n",
            "schema": {
              "type": "string",
              "minLength": 3,
              "example": "Nokia"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for name searches, 25 results per page. Pages beyond 20 are refused with a 400; narrow the name instead.\n",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing both `businessId` and `name`, `name` shorter than 3 characters, or `page` outside 1..20.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/companies/{businessId}": {
      "get": {
        "operationId": "getCompany",
        "tags": [
          "companies"
        ],
        "summary": "Look up a single company by exact business ID",
        "parameters": [
          {
            "name": "businessId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{7}-\\d$",
              "example": "0112038-9"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The company, with `source` and the full `_links` set",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Company"
                }
              }
            }
          },
          "404": {
            "description": "No company with that business ID in the current dataset. The problem carries `dataset`, and `detail` distinguishes two cases: the register is still loading (`dataset.complete` false), so the ID may simply not be in yet; or the register is complete and the ID is not in it, typically a toiminimi (sole proprietorship) outside the Trade Register, or a non-existent ID.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "Link": {
        "description": "RFC 8288 web links: rel=\"service-desc\" to the RFC 9727 API catalog and to this OpenAPI document, rel=\"describedby\" to SKILL.md and llms.txt.\n",
        "schema": {
          "type": "string",
          "example": "<https://ytj.23signs.org/.well-known/api-catalog>; rel=\"service-desc\"; type=\"application/linkset+json\", <https://ytj.23signs.org/openapi.yaml>; rel=\"service-desc\"; type=\"application/yaml\""
        }
      }
    },
    "schemas": {
      "Link": {
        "type": "object",
        "description": "A HATEOAS link.",
        "required": [
          "href"
        ],
        "properties": {
          "href": {
            "type": "string",
            "format": "uri-reference"
          },
          "method": {
            "type": "string",
            "example": "GET"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Media type of the target"
          },
          "templated": {
            "type": "boolean",
            "description": "True when `href` is an RFC 6570 URI template"
          }
        }
      },
      "Links": {
        "type": "object",
        "description": "Navigation links present on every JSON response. `self` is the canonical URL of the current resource; on a company it is /api/v1/companies/{businessId}. Search responses add `next` when `hasMore` is true and `prev` when `page` is greater than 1.\n",
        "required": [
          "self",
          "collection",
          "openapi",
          "catalog"
        ],
        "properties": {
          "self": {
            "$ref": "#/components/schemas/Link"
          },
          "collection": {
            "$ref": "#/components/schemas/Link"
          },
          "search": {
            "$ref": "#/components/schemas/Link"
          },
          "openapi": {
            "$ref": "#/components/schemas/Link"
          },
          "catalog": {
            "$ref": "#/components/schemas/Link"
          },
          "describedby": {
            "$ref": "#/components/schemas/Link"
          },
          "next": {
            "$ref": "#/components/schemas/Link"
          },
          "prev": {
            "$ref": "#/components/schemas/Link"
          }
        },
        "additionalProperties": {
          "$ref": "#/components/schemas/Link"
        }
      },
      "Dataset": {
        "type": "object",
        "description": "What is behind the API. `backend` is `d1` in production (the full Trade Register) and `sample` in local dev (212 bundled companies). `updatedAt` and `pending` are only present on the D1 backend; `note` only on the sample backend.\n",
        "required": [
          "backend",
          "source",
          "companies",
          "snapshotDate",
          "complete"
        ],
        "properties": {
          "backend": {
            "type": "string",
            "enum": [
              "d1",
              "sample"
            ]
          },
          "source": {
            "type": "string",
            "example": "PRH YTJ open data v3 (avoindata.prh.fi), daily all_companies dump"
          },
          "companies": {
            "type": "integer",
            "description": "Companies currently queryable",
            "example": 464751
          },
          "snapshotDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Date of the PRH all_companies dump the data comes from; null on the sample backend"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the crawler last pushed to D1 (UTC). D1 backend only."
          },
          "complete": {
            "type": "boolean",
            "description": "True once every company in the dump has been pushed. While false, a 404 or an empty search may only mean \"not loaded yet\".\n"
          },
          "pending": {
            "type": "integer",
            "description": "Companies in the dump not yet pushed to D1. D1 backend only."
          },
          "note": {
            "type": "string",
            "description": "Sample backend only, explains that the deployed service holds the full register."
          }
        }
      },
      "DatasetResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Dataset"
          },
          {
            "type": "object",
            "required": [
              "scope",
              "_links"
            ],
            "properties": {
              "scope": {
                "type": "string",
                "description": "Trade Register scope note (no toiminimi outside the register)"
              },
              "_links": {
                "$ref": "#/components/schemas/Links"
              }
            }
          }
        ]
      },
      "Address": {
        "type": "object",
        "properties": {
          "street": {
            "type": [
              "string",
              "null"
            ]
          },
          "postCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Company": {
        "type": "object",
        "properties": {
          "businessId": {
            "type": [
              "string",
              "null"
            ],
            "example": "0112038-9"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Nokia Oyj"
          },
          "auxiliaryNames": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "companyForm": {
            "type": [
              "string",
              "null"
            ],
            "example": "Public limited company"
          },
          "status": {
            "type": "string",
            "description": "`active` when PRH's trade register status code is 1, otherwise `not_currently_registered`. Kept two-valued for existing clients; see `registerStatus` for the finer code.\n",
            "enum": [
              "active",
              "not_currently_registered"
            ]
          },
          "registerStatus": {
            "type": "string",
            "description": "PRH trade register status, code list REK_KDI: 0 unregistered, 1 registered, 2 removed_from_register, 3 startup_not_registered, 4 ceased. Any other code is reported as unknown.\n",
            "enum": [
              "unregistered",
              "registered",
              "removed_from_register",
              "startup_not_registered",
              "ceased",
              "unknown"
            ]
          },
          "registrationDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "mainBusinessLine": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "lastModified": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "description": "Only on single lookups (GET /api/v1/companies/{businessId}), not on search result items.\n",
            "example": "PRH YTJ open data v3 (avoindata.prh.fi)"
          },
          "_links": {
            "description": "Full link set on a single-company response. Inside a search result list only `self` (the canonical company URL) is present.\n",
            "$ref": "#/components/schemas/Links"
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "query",
          "pageSize",
          "hasMore",
          "results",
          "dataset",
          "note",
          "_links"
        ],
        "properties": {
          "query": {
            "type": "object",
            "properties": {
              "businessId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "page": {
                "type": "integer",
                "minimum": 1,
                "maximum": 20
              }
            }
          },
          "totalResults": {
            "type": "integer",
            "description": "Present for `businessId` searches (0 or 1) and on the sample backend. Absent for name searches on the D1 backend; use `hasMore` and `_links.next` instead.\n"
          },
          "pageSize": {
            "type": "integer",
            "const": 25
          },
          "hasMore": {
            "type": "boolean",
            "description": "True when another page of results exists"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Company"
            }
          },
          "dataset": {
            "$ref": "#/components/schemas/Dataset"
          },
          "note": {
            "type": "string",
            "description": "Human-readable summary of the dataset, including load progress while incomplete"
          },
          "_links": {
            "$ref": "#/components/schemas/Links"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with recovery extensions.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "example": "https://ytj.23signs.org/errors/NOT_FOUND"
          },
          "title": {
            "type": "string",
            "enum": [
              "BAD_REQUEST",
              "NOT_FOUND",
              "INTERNAL_ERROR"
            ]
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string",
            "description": "Path and query of the request that failed"
          },
          "availableEndpoints": {
            "type": "array",
            "description": "Present on 404s under /api/",
            "items": {
              "type": "string"
            }
          },
          "availableDocuments": {
            "type": "array",
            "description": "Present on 404s under /.well-known/",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "dataset": {
            "description": "Present on a 404 for /api/v1/companies/{businessId}, so the client can tell \"not loaded yet\" from \"not in the register\"",
            "$ref": "#/components/schemas/Dataset"
          },
          "example": {
            "type": "string",
            "description": "Present on the 400 for missing parameters, a request that would succeed"
          },
          "_links": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Link"
            }
          }
        }
      }
    }
  }
}
