openapi: 3.1.0
info:
  title: Companize API
  version: 1.0.0
  summary: Dutch business data - company search and profiles, address validation and VAT validation, in one API.
  description: |
    Companize is three APIs behind one key: **company data** (search, autocomplete and
    registry profiles from the Handelsregister, plus an enrichment layer with websites,
    e-mail addresses, phone numbers, social profiles, employee counts, key financials,
    SBI codes and registry publications), **address validation** (postcode + house number
    lookup, free-text address search and autocomplete over Dutch addresses) and
    **VAT validation** (BTW numbers checked live against the EU VIES register and resolved
    to the company behind them).

    ## Conventions

    - **Envelope.** Every response is `{ "data": ..., "meta": ... }`. `data` describes the
      resource(s); `meta` describes the *request* (request id, timing, pagination info, and,
      on metered calls, credit usage). Recency of the data itself (`asOf`, `source`, `stale`)
      lives *inside* the resource, never in `meta`.
    - **Identifiers are strings.** `kvkNumber` (8 digits), `establishmentNumber` (12 digits, Handelsregister: vestigingsnummer) and
      `rsin` (9 digits) preserve leading zeros and must be treated as strings, never integers.
    - **Metering.** Autocomplete, basic profile fetches and enrichment are each counted against
      your plan's monthly allowance. Enrichment responses additionally return
      `Companize-Credits-Cost` and `Companize-Credits-Remaining`. When an allowance is exhausted
      you receive `402 plan_limit_reached` (naming which limit) until the period resets or you
      upgrade.
    - **Unmetered extras.** Address validation and VAT validation are included with paid plans
      and consume no monthly credits; on trial plans they return `402 paid_plan_required`.
      VAT lookups have their own modest rate ceiling to protect the shared VIES upstream,
      with repeat lookups served from cache and a last-known answer (marked `stale: true`)
      when VIES itself is unreachable.
    - **Rate limiting.** All responses carry `RateLimit-Limit`, `RateLimit-Remaining` and
      `RateLimit-Reset`. Exceeding the per-second limit returns `429` with `Retry-After`.
    - **Errors.** Failures are RFC 9457 `application/problem+json` documents; each `type` URI
      resolves to a documentation page describing that error.

    ## Out of scope for this version (planned)

    On-demand fresh re-scrapes (`fresh=true`), asynchronous batch enrichment, and
    change-monitoring webhooks are intentionally not part of v1.
  contact:
    name: Companize
    url: https://companize.nl

servers:
  - url: https://api.companize.nl/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Search
    description: Autocomplete and filtered company search.
  - name: Companies
    description: Company registry profiles (by KVK number).
  - name: Addresses
    description: Dutch address autocomplete and postcode + house-number resolution.
  - name: Establishments
    description: Establishment (vestiging) profiles.
  - name: Enrichment
    description: Cached enrichment data. Metered.
  - name: VAT
    description: VAT (BTW) validation and resolution. Metered.
  - name: Account
    description: Plan, limits and usage.

paths:
  /autocomplete:
    get:
      tags: [Search]
      summary: Autocomplete companies by name
      description: Low-latency typeahead returning minimal fields. Counts against your autocomplete allowance.
      operationId: autocomplete
      parameters:
        - name: q
          in: query
          required: true
          description: Partial company name.
          schema: { type: string, minLength: 2, example: "acme amst" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 25, default: 10 }
      responses:
        "200":
          description: Suggestions.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutocompleteResponse" }
              example:
                data:
                  - kvkNumber: "01234567"
                    name: "Acme Amsterdam B.V."
                    city: "Amsterdam"
                    type: "hoofdvestiging"
                meta: { requestId: "req_a1b2c3", tookMs: 38 }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /addresses/autocomplete:
    get:
      tags: [Addresses]
      summary: Autocomplete and resolve Dutch addresses
      description: |
        Typeahead and resolver over Dutch addresses, returning concrete resolved addresses. The
        query is parsed for an optional postcode, house number and suffix:

        - `Damrak` - addresses from the lowest house number up
        - `Damrak 47` - the one street segment covering 47, as a concrete address
        - `1012 LK 47A` - resolve that exact postcode + number, echoing `47A`
        - `1012 LK` - that postcode's addresses from the lowest house number up

        The postcode is normalized (`1012 lg` -> `1012LG`). A house number outside every matching
        segment's range (or an unknown postcode) returns no results. Included with paid plans;
        not metered (address calls consume no monthly credits), authenticated and rate-limited.
      operationId: autocompleteAddresses
      parameters:
        - name: q
          in: query
          required: true
          description: Partial address - a street or postcode, optionally a house number (with suffix), optionally a city.
          schema: { type: string, minLength: 3, example: "Damrak 47 Amsterdam" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 25, default: 10 }
      responses:
        "200":
          description: Matching addresses.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AddressListResponse" }
              example:
                data:
                  - postcode: "1012 LK"
                    houseNumber: "47"
                    street: "Damrak"
                    city: "Amsterdam"
                    municipality: "Amsterdam"
                    country: "NL"
                meta: { requestId: "req_a1b2c3", tookMs: 12 }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaidPlanRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /companies:
    get:
      tags: [Search, Companies]
      summary: Search companies
      description: Filtered, page-paginated company search. Pagination depth is capped per plan (see the 400 response); deep enumeration of the full dataset is the bulk/export product, not search. Counts against your basic-fetch allowance.
      operationId: searchCompanies
      parameters:
        - name: name
          in: query
          schema: { type: string }
        - name: city
          in: query
          schema: { type: string }
        - name: postcode
          in: query
          schema: { type: string, example: "1015CJ" }
        - name: sbi
          in: query
          description: SBI activity code.
          schema: { type: string, example: "62010" }
        - name: legalForm
          in: query
          schema: { type: string, example: "bv" }
        - name: status
          in: query
          schema: { type: string, enum: [active, inactive] }
        - { $ref: "#/components/parameters/PageSize" }
        - { $ref: "#/components/parameters/Page" }
      responses:
        "200":
          description: A page of matching companies.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyListResponse" }
              example:
                data:
                  - kvkNumber: "69599084"
                    name: "Coolblue B.V."
                    city: "Rotterdam"
                    legalForm: "Besloten Vennootschap"
                    status: "active"
                    type: "hoofdvestiging"
                  - kvkNumber: "24502593"
                    name: "Coolblue Webshops B.V."
                    city: "Rotterdam"
                    legalForm: "Besloten Vennootschap"
                    status: "active"
                    type: "hoofdvestiging"
                meta: { requestId: "req_a1b2c3", tookMs: 41, page: 1, pageSize: 25, hasMore: false }
        "400":
          description: Malformed request, or pagination depth beyond the plan's ceiling.
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
              examples:
                invalidRequest:
                  summary: Malformed request
                  value:
                    type: "https://companize.nl/docs/errors/invalid_request"
                    title: "Invalid request"
                    status: 400
                    detail: "Unknown query parameter 'foo'."
                paginationLimitReached:
                  summary: Pagination depth exceeded
                  value:
                    type: "https://companize.nl/docs/errors/pagination_limit_reached"
                    title: "Pagination limit reached"
                    status: 400
                    detail: "Requested page exceeds this plan's pagination ceiling of 1,000 results. Narrow the query, or use bulk export for the full set."
                    upgradeUrl: "https://app.companize.nl/billing/upgrade"
                    contactUrl: "https://companize.nl/talk-to-us"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /companies/{kvkNumber}:
    get:
      tags: [Companies]
      summary: Get a company's basic profile
      description: |
        Returns the registry profile. Add `include=enrichment` to also return the enrichment
        block in one round trip; that consumes one enrichment credit and returns the credit
        headers. Counts against your basic-fetch allowance.
      operationId: getCompany
      parameters:
        - { $ref: "#/components/parameters/KvkNumber" }
        - name: include
          in: query
          description: Set to `enrichment` to embed the enrichment block (metered).
          schema: { type: string, enum: [enrichment] }
      responses:
        "200":
          description: The company profile.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            Companize-Credits-Cost:
              $ref: "#/components/headers/CreditsCost"
            Companize-Credits-Remaining:
              $ref: "#/components/headers/CreditsRemaining"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyResponse" }
              example:
                data:
                  kvkNumber: "01234567"
                  legalName: "Acme Amsterdam B.V."
                  tradeNames: ["Acme", "Acme Amsterdam"]
                  legalForm: "bv"
                  status: "active"
                  incorporationDate: "2014-03-11"
                  sbiCodes:
                    - { code: "62010", description: "Ontwerpen en programmeren van computerprogramma's" }
                  mainEstablishment:
                    establishmentNumber: "000051435020"
                    isMainEstablishment: true
                    visitingAddress:
                      { street: "Keizersgracht", houseNumber: "123", postcode: "1015CJ", city: "Amsterdam", country: "NL" }
                    postalAddress:
                      { street: "Postbus", houseNumber: "4321", postcode: "1000AB", city: "Amsterdam", country: "NL" }
                  establishmentCount: 1
                  enrichment: null
                meta: { requestId: "req_a1b2c3", tookMs: 22 }
        "402": { $ref: "#/components/responses/LimitReached" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /companies/{kvkNumber}/establishments:
    get:
      tags: [Establishments, Companies]
      summary: List a company's establishments
      operationId: listEstablishments
      parameters:
        - { $ref: "#/components/parameters/KvkNumber" }
        - { $ref: "#/components/parameters/PageSize" }
        - { $ref: "#/components/parameters/Page" }
      responses:
        "200":
          description: A page of establishments.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EstablishmentListResponse" }
              example:
                data:
                  - establishmentNumber: "000019625863"
                    kvkNumber: "69599084"
                    isMainEstablishment: true
                    tradeNames: ["Coolblue"]
                    visitingAddress:
                      street: "Weena 664"
                      houseNumber: "664"
                      postcode: "3012 CN"
                      city: "Rotterdam"
                      country: "NL"
                    postalAddress: null
                meta: { requestId: "req_d4e5f6", tookMs: 22, page: 1, pageSize: 25, hasMore: false }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /companies/{kvkNumber}/enrichment:
    get:
      tags: [Enrichment]
      summary: Get cached enrichment for a company
      description: |
        Served from the cache. Consumes one enrichment credit, counted against your plan's
        monthly enrichment allowance. The payload carries its own `asOf`/`source`/`stale`
        recency markers.
      operationId: getEnrichment
      parameters:
        - { $ref: "#/components/parameters/KvkNumber" }
      responses:
        "200":
          description: Enrichment data.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            Companize-Credits-Cost: { $ref: "#/components/headers/CreditsCost" }
            Companize-Credits-Remaining: { $ref: "#/components/headers/CreditsRemaining" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnrichmentResponse" }
              example:
                data:
                  website: ["https://acme.nl"]
                  socialProfiles: { linkedin: "https://linkedin.com/company/acme", facebook: null, instagram: null, x: null }
                  email: ["info@acme.nl"]
                  phone: []
                  rsin: "812345678"
                  vat: { number: "NL001234567B01", valid: true, checkedAt: "2026-06-14T09:00:00Z" }
                  employees: { count: 48, band: "20-49" }
                  financials:
                    - { year: 2024, revenue: 1250000, totalAssets: 1680000 }
                    - { year: 2023, revenue: 990000, totalAssets: 1420000 }
                  people: null
                  asOf: "2026-06-10T02:00:00Z"
                  source: "cache"
                  stale: false
                meta: { requestId: "req_d4e5f6", tookMs: 31, creditsCost: 1, creditsRemaining: 1742 }
        "402": { $ref: "#/components/responses/LimitReached" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /establishments/{establishmentNumber}:
    get:
      tags: [Establishments]
      summary: Get an establishment profile
      operationId: getEstablishment
      parameters:
        - { $ref: "#/components/parameters/EstablishmentNumber" }
      responses:
        "200":
          description: The establishment.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EstablishmentResponse" }
              example:
                data:
                  establishmentNumber: "000019625863"
                  kvkNumber: "69599084"
                  isMainEstablishment: true
                  tradeNames: ["Coolblue"]
                  visitingAddress:
                    street: "Weena 664"
                    houseNumber: "664"
                    postcode: "3012 CN"
                    city: "Rotterdam"
                    country: "NL"
                  postalAddress: null
                meta: { requestId: "req_g7h8i9", tookMs: 18 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /vat/{vatNumber}:
    get:
      tags: [VAT]
      summary: Validate a VAT (BTW) number and resolve it to a company
      description: |
        Validates a VAT (BTW) number against the EU VIES service and, when the trader name it
        returns confidently matches a company we hold, resolves it to that KVK entity and records
        the validation on file (`kvkNumber` is null when no confident match is found).

        **Included with paid plans, not metered.** Because the result comes from the external VIES
        service - which can be slow, throttled or temporarily down - this endpoint doesn't consume
        an allowance. When VIES
        can't be reached, the last known answer (up to 7 days old) is served instead, marked
        `stale: true` in `meta` with `checkedAt` reflecting the original check; only when no prior
        answer exists does the response become `503 vies_unavailable` (retry shortly), which is
        distinct from a number that VIES reports as invalid. Every response carries a `disclaimer`
        in `meta` noting the VIES dependency.

        **VIES protection.** To keep the shared upstream healthy, lookups that reach VIES are capped
        independently of your plan's rate limit: 1/second per key plus a global ceiling across all
        keys (`429 vies_throttled` when exceeded). Results are cached for up to 24 hours, so repeat
        lookups of the same number are served instantly, don't count toward these caps, and are
        marked `cached: true` in `meta` with `checkedAt` reflecting the original VIES lookup.
      operationId: validateVat
      parameters:
        - { $ref: "#/components/parameters/VatNumber" }
      responses:
        "200":
          description: Validation result (the number may be valid or invalid; see `data.valid`).
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VatResponse" }
              example:
                data:
                  vatNumber: "NL001234567B01"
                  valid: true
                  kvkNumber: "01234567"
                  name: "Acme Amsterdam B.V."
                  countryCode: "NL"
                  source: "VIES"
                  checkedAt: "2026-07-02T10:15:00Z"
                meta:
                  requestId: "req_a1b2c3"
                  tookMs: 240
                  cached: false
                  disclaimer: "VAT validation is performed against the external EU VIES service and reflects its response. Availability and results depend on VIES and are outside our control."
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaidPlanRequired" }
        "429":
          description: Rate limited - either your plan's request rate, or the dedicated VIES lookup ceiling (`vies_throttled`) that protects the shared upstream service. Cached repeat lookups don't count toward the VIES ceiling.
          headers:
            Retry-After: { schema: { type: integer, example: 1 } }
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
              example:
                type: "https://companize.nl/docs/errors/vies_throttled"
                title: "VAT validation rate limit exceeded"
                status: 429
                detail: "VAT lookups are capped at 1/second per key, with a shared global ceiling, to protect the external VIES service. Repeat lookups of the same number are served from cache and don't count. Retry shortly."
        "503":
          description: The EU VIES service was unreachable; retry shortly.
          headers:
            Retry-After: { schema: { type: integer, example: 5 } }
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
              example:
                type: "https://companize.nl/docs/errors/vies_unavailable"
                title: "VAT validation unavailable"
                status: 503
                detail: "The EU VIES service did not respond. VAT validation depends on VIES; please retry shortly."

  /usage:
    get:
      tags: [Account]
      summary: Current period usage and remaining allowances
      operationId: getUsage
      responses:
        "200":
          description: Usage for the current billing period.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UsageResponse" }
              example:
                data:
                  plan: "growth"
                  period: { start: "2026-06-01T00:00:00Z", resetsAt: "2026-07-01T00:00:00Z" }
                  autocomplete: { used: 4120, included: 30000 }
                  basicFetch: { used: 1875, included: 25000 }
                  enrichment: { used: 412, included: 1500 }
                meta: { requestId: "req_g7h8i9" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me:
    get:
      tags: [Account]
      summary: Plan and limits for the authenticated key
      operationId: getMe
      responses:
        "200":
          description: Account details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountResponse" }
              example:
                data:
                  plan: "growth"
                  rateLimitPerSecond: 15
                  pageSizeMax: 50
                meta: { requestId: "req_j1k2l3" }
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key as a Bearer token, e.g. `Authorization: Bearer ck_live_...`."

  parameters:
    KvkNumber:
      name: kvkNumber
      in: path
      required: true
      description: 8-digit KVK number (string; leading zeros significant).
      schema: { type: string, pattern: "^[0-9]{8}$", example: "01234567" }
    EstablishmentNumber:
      name: establishmentNumber
      in: path
      required: true
      description: "12-digit establishment number (string; leading zeros significant). Handelsregister term: vestigingsnummer."
      schema: { type: string, pattern: "^[0-9]{12}$", example: "000051435020" }
    VatNumber:
      name: vatNumber
      in: path
      required: true
      schema: { type: string, example: "NL001234567B01" }
    Page:
      name: page
      in: query
      description: 1-based page number. The deepest reachable page is bounded by your plan's pagination ceiling; see the search endpoint.
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: pageSize
      in: query
      description: Results per page. Capped by your plan's maximum.
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }

  headers:
    RateLimitLimit:
      description: Requests permitted in the current window.
      schema: { type: integer }
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    RateLimitReset:
      description: Unix epoch seconds when the window resets.
      schema: { type: integer }
    RetryAfter:
      description: Seconds to wait before retrying.
      schema: { type: integer }
    CreditsCost:
      description: Enrichment credits this call consumed.
      schema: { type: integer }
    CreditsRemaining:
      description: Enrichment credits remaining in the period.
      schema: { type: integer }

  responses:
    BadRequest:
      description: Malformed request.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: "https://companize.nl/docs/errors/invalid_request"
            title: "Invalid request"
            status: 400
            detail: "Query parameter 'q' must be at least 2 characters."
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: "https://companize.nl/docs/errors/unauthorized"
            title: "Unauthorized"
            status: 401
            detail: "Provide a valid API key as a Bearer token."
    NotFound:
      description: Resource not found.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: "https://companize.nl/docs/errors/not_found"
            title: "Not found"
            status: 404
            detail: "No company with KVK number 01234567."
    PaidPlanRequired:
      description: This API is included with paid plans only (trials get the metered core API), or there is no active subscription.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: "https://companize.nl/docs/errors/paid_plan_required"
            title: "Paid plan required"
            status: 402
            detail: "This API is included with paid plans only."
            upgradeUrl: "https://app.companize.nl/billing/upgrade"
            contactUrl: "https://companize.nl/talk-to-us"
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
        RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
        RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
        RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: "https://companize.nl/docs/errors/rate_limited"
            title: "Rate limit exceeded"
            status: 429
            detail: "You have exceeded 15 requests/second."
    LimitReached:
      description: A monthly plan allowance has been exhausted, or there is no active subscription.
      headers:
        Companize-Credits-Remaining: { $ref: "#/components/headers/CreditsRemaining" }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          examples:
            planLimitReached:
              summary: Allowance exhausted
              value:
                type: "https://companize.nl/docs/errors/plan_limit_reached"
                title: "Enrichment limit reached"
                status: 402
                detail: "You've used your enrichment allowance for this period."
                limit: "enrichment"
                resetsAt: "2026-07-01T00:00:00Z"
                upgradeUrl: "https://app.companize.nl/billing/upgrade"
                contactUrl: "https://companize.nl/talk-to-us"
            noActiveSubscription:
              summary: No active subscription
              value:
                type: "https://companize.nl/docs/errors/no_active_subscription"
                title: "No active subscription"
                status: 402
                detail: "Your account has no active subscription for the API."
                upgradeUrl: "https://app.companize.nl/billing/upgrade"
                contactUrl: "https://companize.nl/talk-to-us"

  schemas:
    RequestMeta:
      type: object
      required: [requestId]
      properties:
        requestId: { type: string, example: "req_a1b2c3" }
        tookMs: { type: integer, example: 41 }
    CollectionMeta:
      allOf:
        - { $ref: "#/components/schemas/RequestMeta" }
        - type: object
          properties:
            page: { type: integer, example: 1 }
            pageSize: { type: integer, example: 25 }
            hasMore: { type: boolean }
    CreditMeta:
      allOf:
        - { $ref: "#/components/schemas/RequestMeta" }
        - type: object
          properties:
            creditsCost: { type: integer, example: 1 }
            creditsRemaining: { type: integer, example: 1742 }

    Problem:
      type: object
      description: RFC 9457 problem detail. Endpoints may add members such as `limit`, `resetsAt`, `upgradeUrl`, `contactUrl`.
      required: [type, title, status]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: ["string", "null"] }
        limit: { type: ["string", "null"], enum: [autocomplete, basicFetch, enrichment, null] }
        resetsAt: { type: ["string", "null"], format: date-time }
        upgradeUrl: { type: ["string", "null"], format: uri }
        contactUrl: { type: ["string", "null"], format: uri }

    Address:
      type: object
      properties:
        street: { type: ["string", "null"] }
        houseNumber: { type: ["string", "null"] }
        postcode: { type: ["string", "null"] }
        city: { type: ["string", "null"] }
        country: { type: string, default: "NL" }

    ResolvedAddress:
      type: object
      description: A concrete Dutch address - one street segment plus a house number.
      required: [postcode, houseNumber]
      properties:
        postcode: { type: string, example: "1012 LK" }
        houseNumber: { type: string, example: "47" }
        street: { type: ["string", "null"], example: "Damrak" }
        city: { type: ["string", "null"], example: "Amsterdam" }
        municipality: { type: ["string", "null"], example: "Amsterdam" }
        country: { type: string, default: "NL" }

    SbiCode:
      type: object
      properties:
        code: { type: string, example: "62010" }
        description: { type: string }

    AutocompleteSuggestion:
      type: object
      required: [kvkNumber, name]
      properties:
        kvkNumber: { type: string, example: "01234567" }
        name: { type: string, example: "Acme Amsterdam B.V." }
        city: { type: ["string", "null"], example: "Amsterdam" }
        type: { type: string, enum: [rechtspersoon, hoofdvestiging, nevenvestiging] }

    CompanySummary:
      type: object
      required: [kvkNumber, name]
      properties:
        kvkNumber: { type: string, example: "01234567" }
        name: { type: string, example: "Acme Amsterdam B.V." }
        city: { type: ["string", "null"] }
        legalForm: { type: ["string", "null"], example: "bv" }
        status: { type: string, enum: [active, inactive] }
        type: { type: string, enum: [rechtspersoon, hoofdvestiging, nevenvestiging] }

    EstablishmentSummary:
      type: object
      required: [establishmentNumber]
      properties:
        establishmentNumber: { type: string, example: "000051435020" }
        isMainEstablishment: { type: boolean }
        visitingAddress:
          oneOf:
            - { $ref: "#/components/schemas/Address" }
            - { type: "null" }
        postalAddress:
          description: Postal (correspondence) address, e.g. a P.O. box ("Postbus"); null when none on record.
          oneOf:
            - { $ref: "#/components/schemas/Address" }
            - { type: "null" }

    Establishment:
      type: object
      required: [establishmentNumber, kvkNumber]
      properties:
        establishmentNumber: { type: string, example: "000051435020" }
        kvkNumber: { type: string, example: "01234567" }
        isMainEstablishment: { type: boolean }
        tradeNames:
          type: array
          items: { type: string }
        visitingAddress:
          oneOf:
            - { $ref: "#/components/schemas/Address" }
            - { type: "null" }
        postalAddress:
          description: Postal (correspondence) address, e.g. a P.O. box ("Postbus"); null when none on record.
          oneOf:
            - { $ref: "#/components/schemas/Address" }
            - { type: "null" }

    Company:
      type: object
      required: [kvkNumber, legalName, legalForm, status]
      properties:
        kvkNumber: { type: string, example: "01234567" }
        legalName: { type: string, example: "Acme Amsterdam B.V." }
        tradeNames:
          type: array
          items: { type: string }
        legalForm: { type: ["string", "null"], example: "bv" }
        status: { type: string, enum: [active, inactive] }
        incorporationDate: { type: ["string", "null"], format: date }
        sbiCodes:
          type: array
          items: { $ref: "#/components/schemas/SbiCode" }
        nonMailingIndicator:
          type: ["boolean", "null"]
          description: True when the company opted out of unsolicited mail/sales contact; null when unknown.
        mainEstablishment: { $ref: "#/components/schemas/EstablishmentSummary" }
        establishmentCount: { type: integer }
        enrichment:
          description: Present only when requested via `include=enrichment`; otherwise null.
          oneOf:
            - { $ref: "#/components/schemas/Enrichment" }
            - { type: "null" }

    Person:
      type: object
      description: Officer/owner. Home addresses are never included.
      required: [name, role]
      properties:
        name: { type: string }
        role: { type: string, example: "Algemeen directeur" }
        since: { type: ["string", "null"], format: date, description: "In office since this date.", example: "2015-01-01" }
        until: { type: ["string", "null"], format: date, description: "Resigned on this date; null while still in office." }

    Vat:
      type: object
      properties:
        number: { type: string, example: "NL001234567B01" }
        valid: { type: boolean }
        checkedAt:
          type: ["string", "null"]
          format: date-time
          description: When VIES verified the number; null when it is on record but unverified.

    Enrichment:
      type: object
      properties:
        website:
          type: array
          description: All websites on record, primary first.
          items: { type: string, format: uri }
          example: ["https://acme.nl"]
        socialProfiles:
          type: object
          properties:
            linkedin: { type: ["string", "null"] }
            facebook: { type: ["string", "null"] }
            instagram: { type: ["string", "null"] }
            x: { type: ["string", "null"] }
        email:
          type: array
          description: All e-mail addresses on record, primary first.
          items: { type: string, format: email }
        phone:
          type: array
          description: All phone numbers on record, primary first.
          items: { type: string }
        logoUrl: { type: ["string", "null"], format: uri }
        rsin: { type: ["string", "null"], example: "812345678", description: "Legal-entity identifier (Rechtspersonen en Samenwerkingsverbanden Informatienummer)." }
        vat:
          oneOf:
            - { $ref: "#/components/schemas/Vat" }
            - { type: "null" }
        employees:
          type: object
          properties:
            count: { type: ["integer", "null"] }
            band: { type: ["string", "null"], example: "20-49" }
        financials:
          type: array
          description: One entry per filed year, newest first.
          items:
            type: object
            properties:
              year: { type: ["integer", "null"], example: 2024 }
              revenue: { type: ["number", "null"] }
              totalAssets: { type: ["number", "null"], description: "Balance-sheet total from the deposited annual accounts.", example: 358280 }
        people:
          description: Officers/owners. Null when not available.
          oneOf:
            - type: array
              items: { $ref: "#/components/schemas/Person" }
            - { type: "null" }
        asOf:
          type: ["string", "null"]
          format: date-time
          description: When this cached record was last refreshed; null when never scraped (then `stale` is true).
        source: { type: string, example: "cache" }
        stale:
          type: boolean
          description: True when the cached record is older than the freshness threshold.

    VatValidation:
      type: object
      properties:
        vatNumber: { type: string, example: "NL001234567B01" }
        valid: { type: boolean }
        kvkNumber: { type: ["string", "null"], example: "01234567", description: "Resolved KVK number, or null when the VIES trader name didn't confidently match a company." }
        name: { type: ["string", "null"], description: "The resolved company's name, or the VIES trader name when unresolved." }
        countryCode: { type: ["string", "null"], example: "NL" }
        source: { type: string, enum: [VIES], description: "The upstream validation source." }
        checkedAt: { type: string, format: date-time }

    Quota:
      type: object
      properties:
        used: { type: integer }
        included: { type: integer }

    Usage:
      type: object
      properties:
        plan: { type: string, example: "growth" }
        period:
          type: object
          properties:
            start: { type: string, format: date-time }
            resetsAt: { type: string, format: date-time }
        autocomplete: { $ref: "#/components/schemas/Quota" }
        basicFetch: { $ref: "#/components/schemas/Quota" }
        enrichment: { $ref: "#/components/schemas/Quota" }

    Account:
      type: object
      properties:
        plan: { type: string, example: "growth" }
        rateLimitPerSecond: { type: integer, example: 15 }
        pageSizeMax: { type: integer, example: 50 }

    AutocompleteResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/AutocompleteSuggestion" }
        meta: { $ref: "#/components/schemas/RequestMeta" }

    CompanyListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/CompanySummary" }
        meta: { $ref: "#/components/schemas/CollectionMeta" }

    AddressListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ResolvedAddress" }
        meta: { $ref: "#/components/schemas/RequestMeta" }

    CompanyResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/Company" }
        meta: { $ref: "#/components/schemas/RequestMeta" }

    EstablishmentResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/Establishment" }
        meta: { $ref: "#/components/schemas/RequestMeta" }

    EstablishmentListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Establishment" }
        meta: { $ref: "#/components/schemas/CollectionMeta" }

    EnrichmentResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/Enrichment" }
        meta: { $ref: "#/components/schemas/CreditMeta" }

    VatResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/VatValidation" }
        meta:
          allOf:
            - { $ref: "#/components/schemas/RequestMeta" }
            - type: object
              properties:
                cached: { type: boolean, description: "True when served from the lookup cache instead of a fresh VIES call; `checkedAt` then reflects the original lookup." }
                stale: { type: boolean, description: "True when VIES could not answer and the last known (older than 24h) answer was served instead; `checkedAt` reflects that original check." }
                disclaimer: { type: string, description: "Notes the VIES dependency; present on every VAT response." }

    UsageResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/Usage" }
        meta: { $ref: "#/components/schemas/RequestMeta" }

    AccountResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/Account" }
        meta: { $ref: "#/components/schemas/RequestMeta" }
