openapi: 3.1.0
info:
  title: skanfirmy.pl — Polish company verification API
  version: "1.4.0"
  description: |
    Free, keyless REST API for verifying Polish companies straight from official
    government registers. No API key, no registration, no daily quota beyond the source
    registers' own. Rate limit (since 2026-09-25): at most 20 requests per 10 seconds from one
    IP address on the data endpoints (`/nip`, `/nips`, `/regon`, `/firma-po-regon`, `/krs`,
    `/vies`, and since 2026-09-26 `POST /rachunek`); above that Cloudflare answers HTTP 429
    for 10 seconds. The MCP server (`/mcp`) has its own limit since 2026-09-28: at most 60
    requests per 10 seconds from one IP address (above that, HTTP 429 with a JSON-RPC error for
    10 seconds). Bots that Cloudflare recognises as verified (including search-engine crawlers) are
    exempt from the 20-per-10-seconds limit on the data endpoints; the MCP limit applies to everyone.

    Changelog:

    - 1.4.0 (2026-09-28) — some numbers may be restricted from presentation (a data-protection
      procedure: a number in an active bucket of the restriction list). `/nip`, `/regon`, `/vies/PL`
      (`result: restricted`), `POST /rachunek`, `/firma-po-regon` (for a company whose NIP is restricted)
      and `/krs/{number}` (for an entity whose NIP, read from the court extract, is restricted) then answer
      200 with the `Restricted` object — a fixed sentence and a link to the Ministry of Finance search, no
      data, and no field about validity, VAT status or presence in the register; no register is queried,
      except that `/krs/{number}` checks the NIP only after fetching the court extract. In `/nips`
      such a number is `{nip, restricted: true}`, counted in `restrictedCount` and in neither `foundCount`
      nor `notFoundCount`. The MCP tools answer the same way. A “restricted” answer covers a group of
      several hundred numbers. It does not mean that the entity is missing from the Register or that it
      has objected. Check it in the Ministry of Finance VAT payer register search. The MCP server has its
      own rate limit (60 requests per 10 seconds from one IP address).
    - 1.3.1 (2026-09-27) — when the Ministry of Finance daily limit for this service is exhausted,
      or the Ministry answers with something other than JSON without a server error (for example a
      temporary block page served with status 200), `/nip`, `/nips` and `POST /rachunek` answer 503
      with `Retry-After` and a `contact` field (`info@skanfirmy.pl`) for integrations that need steady
      access at a larger scale. On `/nip` and `/nips` such an answer used to end in a bare 500. A server
      error from the Ministry still gives 502, and a non-JSON answer from VIES now gives 502 too.
    - 1.3.0 (2026-09-27) — natural persons running a business get no REGON number, VAT
      registration date or PKD codes: `regon` and `registrationLegalDate` are omitted on `/nip`
      and `/nips` (listed in `privacy.hidden`), and `ceidg` is always `null` (deprecated; the
      PKD codes of natural persons are no longer fetched from GUS). `/regon/{nip}` returns for
      them only `nip`, `nazwa`, `typ`, `forma`, `miejscowosc` and `status` (`active` / `ended`)
      in `dane`, with no top-level `regon`; `/firma-po-regon/{regon}` returns no data at all for
      a natural person's REGON (`dane: null`, no `nip`), only a referral to the GUS search in
      `privacy`. The JSON-LD of the `/nips` HTML page no longer lists the REGON or founding date
      of natural persons.
    - 1.2.0 (2026-09-26) — data of natural persons running a business is limited (GDPR data
      minimisation, see "Natural persons" below): account count and `accountCheck` instead of
      `accountNumbers`, the town instead of the address, REGON address without street,
      numbers and postal code, VIES address of PL numbers reduced to the town, a `privacy`
      object, `X-Robots-Tag: noindex` and a private cache. New `POST /rachunek` (check one bank
      account). `Vary: Accept` on every endpoint that serves both HTML and JSON. The document now
      also lists what these endpoints already returned: `ceidg` and `source: bl+ceidg` on `/nip`,
      `checkedAt`, `mfRequestDateTime`, `invalidCount`, `notFoundCount` and `invalidInput` on
      `/nips`, the 429, 502 and 503 responses missing from some paths, and `Retry-After` on
      503; 429 comes only from the per-IP limit (the Ministry of Finance daily limit is
      answered with 503). The path parameter of `/regon/{nip}` is now declared as `nip`.
    - 1.1.0 (2026-09-25) — per-IP rate limit documented.
    - 1.0.0 — first version.

    The lookup endpoints return human-friendly HTML by default and machine JSON when you
    ask for it, either with `?format=json` or an `Accept: application/json` header
    (`/krs` and `POST /rachunek` always return JSON).

    Natural persons (since 2026-09-26). A White List entity without a KRS number (a sole
    trader or a civil partnership), a REGON entity of type `F` or `LF`, and a civil
    partnership in REGON (filed as type `P`, recognised by its name: "S.C." / "spółka
    cywilna" as the last legal-form marker) is treated as a natural person running a business. Its name, address and bank accounts are personal
    data, so responses carry only what is needed to verify a counterparty:

    - bank accounts: instead of the `accountNumbers` list, their number (`accountCount`)
      and `accountCheck`, which says how to check one specific account with `POST /rachunek`
      (the answer `TAK`/`NIE` comes straight from the Ministry of Finance);
    - White List address: reduced to the town in `city`, and `address` is `null`. The White
      List does not say whether a natural person's address is a place of business or a home
      (VAT Act art. 96b(3)(7): the fixed place of business or, when there is none, the home
      address), so this applies to `residenceAddress` and `workingAddress` alike;
    - REGON number, VAT registration date and PKD codes (since 2026-09-27): not returned on
      `/nip` and `/nips` (`regon` and `registrationLegalDate` omitted, `ceidg` always `null`);
    - REGON (`/regon`): since 2026-09-27 `dane` carries only `nip`, `nazwa`, `typ`, `forma`,
      `miejscowosc` and `status` (`active` / `ended`) — no REGON number, address, municipality,
      county, voivodeship or dates (since 2026-09-26 no street, numbers or postal code);
      `/firma-po-regon` returns no data for a natural person's REGON (`dane: null`), only the
      referral to the GUS search in `privacy`;
    - VIES: it does not say whether a Polish number belongs to a natural person, so for every
      `PL` number `address` is `null` and `city` carries the town; other countries are unchanged;
    - the `privacy` object says what was withheld (`hidden`), why (`reason`, in Polish) and
      where the source register publishes the full entry (`officialSource`);
    - such responses, HTML and JSON alike, are sent with `X-Robots-Tag: noindex` and
      `Cache-Control: private, max-age=3600`; the HTML page shows the name only in the page
      body, not in the title, meta description, Open Graph tags or JSON-LD.

    On `/nip`, `/nips`, `/regon` and `/firma-po-regon`, entities with a KRS number and other
    REGON entities of types `P` / `LP` (not civil partnerships) are unchanged (including the
    full `accountNumbers` list on `/nip` and `/nips`); for them `privacy` is `null`.

    Values that come verbatim from a government register (e.g. the VAT status
    literal `Czynny` / `Zwolniony`) are kept unchanged, and language-neutral derived
    fields are added alongside (e.g. `vatActive`, `statusVatCode`) so agents can
    branch on a stable value without parsing Polish.

    Sources: Ministry of Finance VAT White List ("Biała Lista"), National Court
    Register (KRS, Ministry of Justice), REGON (GUS), and the EU VIES system.

    There is also a Model Context Protocol server at `POST /mcp` (JSON-RPC 2.0) for
    AI agents; it is not described in this OpenAPI document. Since its version 1.2.0
    (2026-09-26) its tools apply the same limits for natural persons; there `accountCheck`
    points to the `sprawdz_rachunek` tool. Since 1.3.0 (2026-09-27) the same REGON and PKD limits
    apply, and its monitoring tools return only VAT status and masked account changes for
    natural persons, with `name` set to `null`.
  contact:
    name: skanfirmy.pl
    url: https://skanfirmy.pl/
  license:
    name: Data from public government registers
    url: https://skanfirmy.pl/regulamin
servers:
  - url: https://skanfirmy.pl
tags:
  - name: company
    description: Company lookups by identifier
  - name: bank-account
    description: Bank account check against the VAT White List
  - name: eu-vat
    description: EU VAT number validation

paths:
  /nip/{number}:
    get:
      tags: [company]
      operationId: getCompanyByNip
      summary: Aggregated company record by NIP
      description: |
        Returns an aggregated record for a Polish company by its 10-digit NIP: VAT status
        from the White List plus, when the entity is in the KRS, selected data from the current
        KRS extract (legal form, share capital, representation body and rule, board functions
        without names, PKD codes) in a single call.
        `ceidg` is deprecated: since 2026-09-27 it is always `null` (the PKD codes of natural
        persons are no longer fetched).

        Since 2026-09-26, for an entity without a KRS number (a natural person running a
        business): `bl.accountNumbers` is replaced by `bl.accountCount` and `bl.accountCheck`,
        the address is reduced to the town in `bl.city`, `privacy` lists what was withheld,
        and the response is sent with `X-Robots-Tag: noindex` and a private cache. Since 2026-09-27
        `bl.regon` and `bl.registrationLegalDate` are omitted for them as well.
      parameters:
        - $ref: "#/components/parameters/Nip"
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Entity found.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Vary: { $ref: "#/components/headers/Vary" }
            X-Robots-Tag: { $ref: "#/components/headers/XRobotsTag" }
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/NipResult" }, { $ref: "#/components/schemas/Restricted" }] }
              examples:
                naturalPerson:
                  summary: Natural person running a business (synthetic data), since 2026-09-27
                  value:
                    nip: "1111111111"
                    source: bl-only
                    bl:
                      nip: "1111111111"
                      name: JAN TESTOWY USŁUGI REMONTOWE
                      statusVat: Czynny
                      vatActive: true
                      statusVatCode: active
                      address: null
                      city: WYMYŚLONOWO
                      krs: null
                      accountCount: 2
                      accountCheck:
                        method: POST
                        url: https://skanfirmy.pl/rachunek
                        body: { nip: "1111111111", nrb: "<26 cyfr rachunku>" }
                      mfRequestDateTime: "26-09-2026 10:15:02"
                    krs: null
                    krsError: null
                    ceidg: null
                    privacy:
                      naturalPerson: true
                      hidden: [accountNumbers, residenceAddress, regon, registrationLegalDate]
                      reason: Dane osoby fizycznej prowadzącej działalność ograniczone do niezbędnych do weryfikacji kontrahenta (RODO). Pełny wpis publikuje rejestr źródłowy (officialSource).
                      officialSource: https://www.gov.pl/web/kas/wykaz-podatnikow-vat
                    checkedAt: "2026-09-26"
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }
        "503": { $ref: "#/components/responses/UpstreamUnavailable" }

  /nips/{lista}:
    get:
      tags: [company]
      operationId: getCompaniesByNipBatch
      summary: Batch VAT lookup for up to 30 NIPs
      description: |
        Comma-separated list of up to 30 NIPs. Returns VAT status and basic data for
        each, with the same raw+derived fields as `/nip`.

        Since 2026-09-26, each natural person in the list (entity without a KRS number) gets
        `accountCount` and `accountCheck` instead of `accountNumbers`, the town instead of the
        address and a short `privacy` object (since 2026-09-27 also no `regon` or
        `registrationLegalDate`); the top-level `privacy` summary counts them.
        When the list contains at least one natural person, the response is sent with
        `X-Robots-Tag: noindex` and a private cache.
      parameters:
        - name: lista
          in: path
          required: true
          description: Comma-separated NIPs (max 30), e.g. `5260250274,7740001454`.
          schema: { type: string }
          example: "5260250274,7740001454"
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Batch processed.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Vary: { $ref: "#/components/headers/Vary" }
            X-Robots-Tag: { $ref: "#/components/headers/XRobotsTag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NipsResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }
        "503": { $ref: "#/components/responses/UpstreamUnavailable" }

  /regon/{nip}:
    get:
      tags: [company]
      operationId: getRegonByNip
      summary: REGON (GUS) registry data by NIP
      description: |
        Official REGON data from the GUS BIR register by NIP — including sole
        traders (JDG), which are not in the KRS.

        Since 2026-09-27, for natural persons (`typ` `F` or `LF`, and civil partnerships filed
        as `P`) `dane` carries only `nip`, `nazwa`, `typ`, `forma`, `miejscowosc` and `status`
        (`active` / `ended`) — no REGON number (also not at the top level), address,
        administrative units or dates; `privacy` lists what was withheld, and the response is sent
        with `X-Robots-Tag: noindex` and a private cache.
      parameters:
        - name: nip
          in: path
          required: true
          description: 10-digit Polish tax identifier (NIP), digits only.
          schema: { type: string, pattern: "^[0-9]{10}$" }
          example: "5260250274"
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Entity found in REGON.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Vary: { $ref: "#/components/headers/Vary" }
            X-Robots-Tag: { $ref: "#/components/headers/XRobotsTag" }
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/RegonResult" }, { $ref: "#/components/schemas/Restricted" }] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /krs/{number}:
    get:
      tags: [company]
      operationId: getKrsByNumber
      summary: KRS extract by KRS number
      description: |
        Selected data from the current KRS extract (from the Ministry of Justice open KRS API) by 10-digit
        KRS number: legal form, share capital, representation body and rule, board functions (without
        names), PKD codes. Always returns JSON (no HTML variant).

        Since 2026-09-28, when the NIP read from the extract is restricted from presentation, the answer is
        the `Restricted` object (with `krs`, the number you asked about) instead of the data.
      parameters:
        - name: number
          in: path
          required: true
          description: 10-digit KRS number (leading zeros allowed, e.g. `0000028860`).
          schema: { type: string, pattern: "^[0-9]{1,10}$" }
          example: "0000028860"
      responses:
        "200":
          description: KRS entry found.
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/KrsResult" }, { $ref: "#/components/schemas/Restricted" }] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /firma-po-regon/{regon}:
    get:
      tags: [company]
      operationId: getCompanyByRegon
      summary: Company identity by REGON number
      description: |
        Resolve a company by its REGON number (9 or 14 digits) via the GUS BIR
        register — returns the official name, NIP, legal form and address. The
        reverse of /regon/{nip} for entities that are not natural persons.

        Since 2026-09-27, a REGON of a natural person (`typ` `F` or `LF`, or a civil partnership
        filed as `P`) returns no data at all: `dane` is `null`, there is no `nip`, and `privacy`
        carries the referral to the GUS REGON search (resolving a person's identity from the
        number alone is not offered).
      parameters:
        - name: regon
          in: path
          required: true
          description: 9- or 14-digit REGON number.
          schema: { type: string, pattern: "^[0-9]{9}([0-9]{5})?$" }
          example: "610188201"
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: Entity found in REGON.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Vary: { $ref: "#/components/headers/Vary" }
            X-Robots-Tag: { $ref: "#/components/headers/XRobotsTag" }
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/RegonResult" }, { $ref: "#/components/schemas/Restricted" }] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /vies/{country}/{number}:
    get:
      tags: [eu-vat]
      operationId: validateEuVat
      summary: Validate an EU VAT number (VIES)
      description: |
        Validates a counterparty's EU VAT number via the European Commission VIES
        service. `result` distinguishes a valid number, a well-formed but
        unregistered one, a malformed input, and a VIES outage — so an agent never
        mistakes downtime for an invalid VAT ID. Since 2026-09-28 it can also be
        `restricted` (`PL` numbers only): the number's data is not presented (see `Restricted`).

        Since 2026-09-26, for Polish (`PL`) numbers `address` is `null` and `city` carries
        the town, because VIES does not say whether the number belongs to a natural person;
        such responses are sent with `X-Robots-Tag: noindex` and a private cache. Numbers of
        other countries are unchanged.
      parameters:
        - name: country
          in: path
          required: true
          description: Two-letter EU country code (e.g. `PL`, `DE`, `FR`; `XI` for Northern Ireland).
          schema: { type: string, minLength: 2, maxLength: 2 }
          example: "PL"
        - name: number
          in: path
          required: true
          description: VAT number without the country prefix.
          schema: { type: string }
          example: "5260250274"
        - $ref: "#/components/parameters/Format"
      responses:
        "200":
          description: VIES answered (see `result` for the outcome).
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            Vary: { $ref: "#/components/headers/Vary" }
            X-Robots-Tag: { $ref: "#/components/headers/XRobotsTag" }
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/ViesResult" }, { $ref: "#/components/schemas/Restricted" }] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamError" }
        "503": { $ref: "#/components/responses/UpstreamUnavailable" }

  /rachunek:
    post:
      tags: [bank-account]
      operationId: checkBankAccount
      summary: Check one bank account against the VAT White List (since 2026-09-26)
      description: |
        Asks the Ministry of Finance whether one bank account (26-digit NRB) is assigned to
        a NIP on the VAT White List (the `check` method of the White List API) and returns its
        answer: the raw literal `TAK` / `NIE`, a derived boolean and the Ministry's request id,
        which serves as proof of the check. Use it for natural persons, whose account list
        `/nip` and `/nips` no longer return (see `accountCheck`), or for any entity when you
        only need to confirm the account on an invoice.

        The NIP and the account number go in the JSON body, not in the URL, so they do not
        appear in traffic statistics, which record URL paths. A wrong NIP or account number
        (length or checksum) is rejected with `400` before any query to the Ministry.
        Responses are never cached (`Cache-Control: no-store`) and are sent with
        `X-Robots-Tag: noindex`.

        `GET /rachunek` is the interactive tool page (HTML), which checks the account straight
        from the visitor's browser; it is not an API.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AccountCheckRequest" }
            example:
              nip: "1111111111"
              nrb: "PL73 1111 1111 1111 1111 1111 1111"
      responses:
        "200":
          description: The Ministry of Finance answered.
          headers:
            Cache-Control:
              description: Always `no-store`.
              schema: { type: string, const: no-store }
            X-Robots-Tag:
              description: Always `noindex`.
              schema: { type: string, const: noindex }
          content:
            application/json:
              schema: { oneOf: [{ $ref: "#/components/schemas/AccountCheckResult" }, { $ref: "#/components/schemas/Restricted" }] }
              example:
                nip: "1111111111"
                nrb: "73111111111111111111111111"
                accountAssigned: NIE
                assigned: false
                requestId: a1b2c-3d4e5f
                mfRequestDateTime: "26-09-2026 10:15:02"
                checkedAt: "2026-09-26"
        "400":
          description: Invalid JSON, or a wrong NIP or account number (length or checksum) — rejected without a query to the Ministry of Finance — or the Ministry rejected the query (`mfCode`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountCheckError" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502":
          description: Network error, an unexpected answer or a server-side error of the Ministry of Finance (`mfCode` when the Ministry sent one).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountCheckError" }
        "503": { $ref: "#/components/responses/UpstreamUnavailable" }

components:
  parameters:
    Nip:
      name: number
      in: path
      required: true
      description: 10-digit Polish tax identifier (NIP), digits only.
      schema: { type: string, pattern: "^[0-9]{10}$" }
      example: "5260250274"
    Format:
      name: format
      in: query
      required: false
      description: "Set to `json` for a JSON response (equivalent to sending an `Accept: application/json` header). Omitted returns HTML."
      schema: { type: string, enum: [json] }

  headers:
    CacheControl:
      description: |
        `public, max-age=86400` for a found entity. Since 2026-09-26
        `private, max-age=3600` when the response contains data of a natural person running
        a business (on `/nips`: when any entity in the list is one; on `/vies`: every `PL`
        number) — only the requester's own browser may keep it, not shared caches.
      schema: { type: string }
    Vary:
      description: "`Accept` (since 2026-09-26): the same URL returns HTML or JSON depending on the `Accept` header, so caches keep the two variants apart."
      schema: { type: string, const: Accept }
    XRobotsTag:
      description: "`noindex`, sent since 2026-09-26 with every response (HTML and JSON) that contains data of a natural person running a business (on `/nips`: when any entity in the list is one; on `/vies`: every `PL` number). Absent otherwise."
      schema: { type: string, const: noindex }
    RetryAfter:
      description: Seconds to wait before retrying (3600 when the Ministry of Finance daily limit is exhausted, 1800 when a member state's registry is down in VIES).
      schema: { type: integer }

  responses:
    BadRequest:
      description: Invalid identifier (wrong length or checksum).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No entity found for this identifier.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests from one IP address (more than 20 per 10 s) — Cloudflare answers 429 for 10 s, possibly with an HTML body; try again later. A source register's daily quota is answered with 503 instead.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UpstreamError:
      description: The source register returned an error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UpstreamUnavailable:
      description: The source register is temporarily unavailable, or the Ministry of Finance daily limit for this service is exhausted (or the Ministry refused the query); retry after `Retry-After` seconds. Ministry of Finance refusals carry a `contact` field.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string, description: Human-readable error message (Polish). }
        contact: { type: string, description: "Only on 503 refusals by the Ministry of Finance (daily limit, block): the address to write to for steady access at a larger scale (`info@skanfirmy.pl`). Since 1.3.1." }
      required: [error]

    VatFields:
      type: object
      description: Raw government literal plus language-neutral derived fields.
      properties:
        statusVat: { type: string, description: "Raw MF literal, verbatim.", examples: ["Czynny", "Zwolniony"] }
        vatActive: { type: boolean, description: "Derived: true only for an active VAT payer (statusVat == 'Czynny')." }
        statusVatCode:
          type: string
          description: "Derived, language-neutral enum."
          enum: [active, exempt, not_registered]

    PkdItem:
      type: object
      properties:
        code: { type: string, example: "19.20.Z" }
        opis: { type: string, description: Official Polish PKD description (never translated). }

    KrsData:
      type: object
      description: Parsed current KRS extract.
      properties:
        numerKRS: { type: string, example: "0000028860" }
        nazwa: { type: string }
        formaPrawna: { type: string, example: "SPÓŁKA AKCYJNA" }
        nip: { type: string }
        regon: { type: string }
        opp: { type: boolean, description: Whether the entity is a public-benefit organisation (OPP). }
        adres: { type: string }
        kapital: { type: [string, "null"], description: Share capital with currency. }
        organ: { type: [string, "null"], description: Representing body, e.g. ZARZĄD. }
        sposobReprezentacji: { type: [string, "null"] }
        sklad:
          type: array
          items: { type: string }
          description: Roles in the representing body (individuals' names are masked in the open KRS API).
        pkdMain:
          type: array
          items: { $ref: "#/components/schemas/PkdItem" }
        pkdOther:
          type: array
          items: { $ref: "#/components/schemas/PkdItem" }
        dataRejestracji: { type: string }
        dataOstatniegoWpisu: { type: string }
        stanZDnia: { type: string }
        dataCzasOdpisu: { type: string }

    CeidgData:
      type: object
      description: "Deprecated since 1.3.0 (2026-09-27): never produced. It carried the PKD codes of an entity without a KRS number from the REGON register (GUS BIR full report of a natural person)."
      properties:
        source: { type: string, const: ceidg-gus }
        pkdMain:
          type: array
          items: { $ref: "#/components/schemas/PkdItem" }
        pkdOther:
          type: array
          items: { $ref: "#/components/schemas/PkdItem" }

    Privacy:
      type: object
      description: Since 2026-09-26 — what was withheld from a response about a natural person running a business (GDPR data minimisation).
      properties:
        naturalPerson:
          type: [boolean, "null"]
          description: "`true` for a natural person (White List entity without a KRS number; REGON type `F` / `LF`, or a civil partnership — recognised by its name, because REGON files it as type `P`); `null` on `/vies` for `PL` numbers, where VIES does not say."
        hidden:
          type: array
          items: { type: string }
          description: "Source fields withheld from this response (only those that had a value), e.g. `accountNumbers`, `residenceAddress`, `workingAddress`, `regon`, `registrationLegalDate` (White List), `regon`, `ulica`, `nrNieruchomosci`, `nrLokalu`, `kodPocztowy`, `gmina`, `powiat`, `wojewodztwo`, `dataZakonczenia`, `silosID` (REGON), `nip`, `nazwa`, `miejscowosc`, `adres` (`/firma-po-regon` referral), `address` (VIES)."
        reason: { type: string, description: Why, in Polish. }
        officialSource:
          type: string
          format: uri
          description: "Official page where the source register publishes the full entry: https://www.gov.pl/web/kas/wykaz-podatnikow-vat (White List, also for VIES `PL` numbers) or https://wyszukiwarkaregon.stat.gov.pl/ (REGON)."
      required: [naturalPerson, hidden, reason, officialSource]

    AccountCheck:
      type: object
      description: |
        Natural persons with at least one account on the White List (since 2026-09-26): how to
        check one specific account. Send `body` to `url` with `method`, replacing the `nrb`
        placeholder with the 26-digit account number you want to check (e.g. from the invoice).
        Sent instead of the account list.
      properties:
        method: { type: string, const: POST }
        url: { type: string, const: "https://skanfirmy.pl/rachunek" }
        body:
          type: object
          properties:
            nip: { type: string, description: The NIP of this entity. }
            nrb: { type: string, description: "Placeholder `<26 cyfr rachunku>` — replace it with the account number to check." }
          required: [nip, nrb]
      required: [method, url, body]

    NipResult:
      type: object
      properties:
        nip: { type: string }
        source:
          type: string
          enum: [krs, bl+ceidg, bl-only]
          description: "`krs` when KRS data was merged in; `bl-only` otherwise. `bl+ceidg` is no longer produced since 1.3.0 (2026-09-27)."
        bl:
          type: object
          description: White List (Biała Lista) data from the Ministry of Finance.
          allOf:
            - $ref: "#/components/schemas/VatFields"
          properties:
            nip: { type: string }
            name: { type: [string, "null"] }
            regon: { type: [string, "null"], description: "Omitted for natural persons (since 2026-09-27)." }
            address:
              type: [string, "null"]
              description: |
                Address from the White List (registered seat or business address). For a natural
                person (since 2026-09-26) always `null` — see `city`.
            city:
              type: string
              description: "Natural persons only (since 2026-09-26): the town from the White List address, sent instead of the address. Omitted when the town cannot be read reliably (no postal code)."
            krs: { type: [string, "null"] }
            registrationLegalDate: { type: [string, "null"], description: "Omitted for natural persons (since 2026-09-27)." }
            accountNumbers:
              type: array
              items: { type: string }
              description: "Bank accounts registered on the White List (26-digit NRB). Since 2026-09-26 only for entities with a KRS number; natural persons get `accountCount` and `accountCheck` instead."
            accountCount:
              type: integer
              minimum: 0
              description: "Natural persons only (since 2026-09-26): how many bank accounts the White List has for this NIP."
            accountCheck: { $ref: "#/components/schemas/AccountCheck" }
            mfRequestDateTime: { type: string }
        krs:
          oneOf:
            - $ref: "#/components/schemas/KrsData"
            - type: "null"
          description: Present only when the entity has a KRS number and the extract was retrieved.
        krsError:
          type: [string, "null"]
          description: Set to the KRS number if the entity has one but its extract could not be fetched.
        ceidg:
          oneOf:
            - $ref: "#/components/schemas/CeidgData"
            - type: "null"
          description: "Deprecated: always `null` since 1.3.0 (2026-09-27); kept so that existing parsers do not break."
        privacy:
          oneOf:
            - $ref: "#/components/schemas/Privacy"
            - type: "null"
          description: Since 2026-09-26 — set for a natural person running a business (entity without a KRS number); `null` otherwise.
        checkedAt: { type: string, format: date }

    Restricted:
      type: object
      description: >-
        Since 2026-09-28: the number is in an active bucket of the presentation-restriction list (a data-protection
        procedure). No data is presented and no register is queried (except on /krs/{number}, which learns the NIP
        from the court extract and checks it after fetching the extract). No field tells whether the number is
        valid, active or registered. A “restricted” answer covers a group of several hundred numbers. It does not
        mean that the entity is missing from the Register or that it has objected. Check it in the Ministry of
        Finance VAT payer register search.
      properties:
        nip: { type: string, description: "The NIP you asked about (not on /firma-po-regon, /krs and /vies)." }
        regon: { type: string, description: "Only on /firma-po-regon: the REGON you asked about." }
        krs: { type: string, description: "Only on /krs: the KRS number you asked about (10 digits)." }
        countryCode: { type: string, description: "Only on /vies." }
        vatNumber: { type: string, description: "Only on /vies." }
        result: { type: string, const: restricted, description: "Only on /vies, where `result` is present in every response." }
        restricted: { type: boolean, const: true }
        info: { type: string, description: A fixed sentence in Polish. }
        officialSource: { type: string, format: uri, description: The Ministry of Finance VAT register search. }
        checkedAt: { type: string, format: date }
      required: [restricted, info, officialSource]

    NipsResult:
      type: object
      properties:
        checkedAt: { type: string, format: date }
        mfRequestDateTime: { type: [string, "null"] }
        total: { type: integer, description: How many items the request contained. }
        validCount: { type: integer, description: How many inputs were valid NIPs. }
        invalidCount: { type: integer, description: How many inputs were not valid NIPs (listed in `invalidInput`). }
        foundCount: { type: integer, description: How many were found in the register. Restricted numbers are not counted here. }
        notFoundCount: { type: integer, description: How many valid NIPs are not in the register. Restricted numbers are not counted here. }
        restrictedCount: { type: integer, description: "Only when present: how many numbers are restricted from presentation (since 2026-09-28)." }
        restrictedInfo:
          type: object
          description: Only with `restrictedCount`. The fixed sentence and the Ministry of Finance search link.
          properties:
            info: { type: string }
            officialSource: { type: string, format: uri }
        results:
          type: array
          items:
            type: object
            allOf:
              - $ref: "#/components/schemas/VatFields"
            properties:
              nip: { type: string }
              restricted: { type: boolean, const: true, description: "Only on a restricted number (since 2026-09-28); the entry then has just `nip` and `restricted`." }
              found: { type: boolean }
              name: { type: [string, "null"] }
              regon: { type: [string, "null"], description: "Omitted for natural persons (since 2026-09-27)." }
              address:
                type: [string, "null"]
                description: As `bl.address` in `/nip`.
              city:
                type: string
                description: As `bl.city` in `/nip` (natural persons only, since 2026-09-26).
              krs: { type: [string, "null"] }
              registrationLegalDate: { type: [string, "null"], description: "Omitted for natural persons (since 2026-09-27)." }
              accountNumbers:
                type: array
                items: { type: string }
                description: "Since 2026-09-26 only for entities with a KRS number."
              accountCount:
                type: integer
                minimum: 0
                description: Natural persons only (since 2026-09-26).
              accountCheck: { $ref: "#/components/schemas/AccountCheck" }
              privacy:
                type: object
                description: Natural persons only (since 2026-09-26); `reason` and `officialSource` are in the top-level `privacy`.
                properties:
                  naturalPerson: { type: boolean, const: true }
                  hidden:
                    type: array
                    items: { type: string }
        invalidInput:
          type: array
          description: Present only when some inputs were not valid NIPs.
          items:
            type: object
            properties:
              input: { type: string }
              reason: { type: string, enum: [not_10_digits, invalid_checksum] }
        privacy:
          type: object
          description: Since 2026-09-26 — present when at least one result is a natural person running a business.
          properties:
            naturalPersons: { type: integer, minimum: 1 }
            reason: { type: string, description: Why, in Polish. }
            officialSource: { type: string, format: uri }

    RegonResult:
      type: object
      properties:
        nip: { type: string }
        source: { type: string, enum: [regon-gus] }
        regon: { type: string, description: "Omitted for natural persons and civil partnerships (since 2026-09-27)." }
        dane:
          type: [object, "null"]
          description: |
            REGON entity data from GUS BIR. Since 2026-09-27, for natural persons (`typ` `F` or
            `LF`, and civil partnerships filed as `P`) only `nip`, `nazwa`, `typ`, `forma`,
            `miejscowosc` and `status` — no REGON number, address, administrative units or dates.
            `null` on `/firma-po-regon` for a natural person's REGON (referral only).
          properties:
            regon: { type: string }
            nip: { type: string }
            nazwa: { type: string }
            wojewodztwo: { type: string }
            powiat: { type: string }
            gmina: { type: string }
            miejscowosc: { type: string }
            kodPocztowy: { type: string, description: "Omitted for types F and LF and for civil partnerships (since 2026-09-26)." }
            ulica: { type: string, description: "Omitted for types F and LF and for civil partnerships (since 2026-09-26)." }
            nrNieruchomosci: { type: string, description: "Omitted for types F and LF and for civil partnerships (since 2026-09-26)." }
            nrLokalu: { type: string, description: "Omitted for types F and LF and for civil partnerships (since 2026-09-26)." }
            typ: { type: string, description: "Entity type, e.g. P (legal person), F (natural person)." }
            forma: { type: string, description: "Natural persons and civil partnerships only (since 2026-09-27): `osoba fizyczna`, `jednostka lokalna osoby fizycznej` or `spółka cywilna`." }
            status: { type: string, enum: [active, ended], description: "Natural persons and civil partnerships only (since 2026-09-27): whether the activity has ended (end date or SilosID 4 in GUS)." }
            silosID: { type: string }
            dataZakonczenia: { type: string }
        privacy:
          oneOf:
            - $ref: "#/components/schemas/Privacy"
            - type: "null"
          description: Since 2026-09-26 — set for types `F` and `LF` and for civil partnerships filed as `P`; `null` otherwise.
        checkedAt: { type: string, format: date }

    KrsResult:
      type: object
      properties:
        source: { type: string, enum: [krs] }
        krs: { $ref: "#/components/schemas/KrsData" }
        checkedAt: { type: string, format: date }

    ViesResult:
      type: object
      properties:
        countryCode: { type: string }
        vatNumber: { type: string }
        valid: { type: boolean, description: "Raw VIES boolean." }
        result:
          type: string
          description: "Derived, language-neutral outcome."
          enum: [valid, not_registered, invalid_format, source_unavailable]
        name: { type: [string, "null"] }
        address:
          type: [string, "null"]
          description: "Address as VIES returns it. Since 2026-09-26 always `null` for `PL` numbers (see `city`)."
        city:
          type: string
          description: "`PL` numbers only (since 2026-09-26): the town taken from the VIES address; absent when VIES returned no address or the town cannot be read from it."
        requestDate: { type: [string, "null"] }
        privacy:
          oneOf:
            - $ref: "#/components/schemas/Privacy"
            - type: "null"
          description: "Since 2026-09-26 — set for `PL` numbers when VIES returned an address (`naturalPerson` is `null`: VIES does not say whether the number belongs to a natural person); `null` otherwise."
        checkedAt: { type: string, format: date }

    AccountCheckRequest:
      type: object
      properties:
        nip: { type: string, description: "10-digit NIP (characters other than digits are ignored)." }
        nrb: { type: string, description: "26-digit Polish bank account number (NRB); spaces, hyphens and a `PL` prefix are accepted." }
      required: [nip, nrb]

    AccountCheckResult:
      type: object
      properties:
        nip: { type: string }
        nrb: { type: string, description: The account number that was checked (26 digits). }
        accountAssigned:
          type: string
          enum: [TAK, NIE]
          description: Raw Ministry of Finance literal, verbatim.
        assigned: { type: boolean, description: "Derived: true only when accountAssigned is 'TAK'." }
        requestId: { type: [string, "null"], description: Ministry of Finance request id — keep it as proof of the check (due diligence). }
        mfRequestDateTime: { type: [string, "null"], description: Ministry of Finance timestamp of the check. }
        checkedAt: { type: string, format: date }
      required: [nip, nrb, accountAssigned, assigned, requestId, mfRequestDateTime, checkedAt]

    AccountCheckError:
      type: object
      properties:
        error: { type: string, description: Human-readable error message (Polish). }
        mfCode: { type: string, description: "Error code from the Ministry of Finance (`WL-…`), when the Ministry rejected the query." }
        contact: { type: string, description: "Only on 503 (Ministry of Finance daily limit or block): `info@skanfirmy.pl`, for steady access at a larger scale. Since 1.3.1." }
      required: [error]
