openapi: 3.1.0
info:
  title: vatnode API
  version: '1.0.0'
  summary: EU VAT validation and rates API
  description: |
    vatnode validates EU VAT IDs against the EU Commission's VIES system with
    national-database fallback, and serves up-to-date VAT rates for every
    EU member state.

    This specification covers the **public API**: VAT validation and VAT
    rates lookup. Account-management endpoints (API keys, subscriptions,
    webhooks, billing) are session-authenticated and used only by the
    vatnode dashboard — they are intentionally not included in this contract.

    ## Authentication

    `/v1/vat/*` requires an API key passed as a Bearer token:

    ```
    Authorization: Bearer vn_live_...
    ```

    Test keys (prefix `vn_test_`) accept only VAT IDs starting with `XX` and
    return deterministic fixtures without hitting VIES or consuming quota.

    `/v1/rates/*` is public and requires no authentication.

    ## Consultation numbers

    When a request to `/v1/vat/{vatId}` includes the optional
    `requesterCountryCode` and `requesterVatNumber` query parameters, vatnode
    calls VIES `checkVatApprox` on behalf of that requester. VIES then issues
    a `consultationNumber` (the EU Commission's `requestIdentifier`) which is
    returned in the response. Stored alongside the invoice, it is the
    strongest audit-grade proof of a VIES validation — tax authorities
    (notably France and Germany) commonly expect it on audit.

    The account-level requester configured in the dashboard is used as a
    default so you do not have to pass these parameters on every call.

    When a requester is supplied, vatnode guarantees one of two outcomes:
    a successful response containing a `consultationNumber`, or an error.
    National-registry fallback is disabled in this mode — a result without
    a consultation number would silently strip the audit evidence the
    caller opted into. Specifically:

    - If VIES rejects the requester (e.g. deregistered, typo), the API
      returns `INVALID_REQUESTER` (HTTP 422).
    - If VIES is unavailable, the API returns `VIES_UNAVAILABLE`
      (HTTP 503) — no national fallback is attempted.

    Callers that prefer maximum validation availability over audit-grade
    evidence should leave the requester unset.
  contact:
    name: vatnode support
    email: support@vatnode.dev
    url: https://vatnode.dev
  license:
    name: Proprietary
    url: https://vatnode.dev/terms

servers:
  - url: https://api.vatnode.dev
    description: Production

tags:
  - name: VAT validation
    description: Validate EU VAT IDs against VIES with national fallback.
  - name: VAT rates
    description: Up-to-date VAT rates for EU member states.

security:
  - ApiKey: []

paths:
  /v1/vat/{vatId}:
    get:
      tags: [VAT validation]
      summary: Validate an EU VAT ID
      description: |
        Validates the supplied VAT ID against VIES, falling back to the
        relevant national registry when VIES is unavailable. Results for the
        base validation are cached for up to 15 minutes.

        Pass `requesterCountryCode` and `requesterVatNumber` (together) to
        obtain a VIES `consultationNumber`. When a requester is supplied the
        cache is bypassed so a fresh consultation number is always issued.
      operationId: validateVat
      security:
        - ApiKey: []
      parameters:
        - $ref: '#/components/parameters/VatIdPath'
        - name: requesterCountryCode
          in: query
          required: false
          description: Two-letter country code of the requesting business.
          schema:
            type: string
            pattern: '^[A-Za-z]{2}$'
            example: DE
        - name: requesterVatNumber
          in: query
          required: false
          description: VAT number of the requesting business (without country prefix).
          schema:
            type: string
            minLength: 1
            maxLength: 20
            example: '123456789'
      responses:
        '200':
          description: Validation result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatValidationResult'
              examples:
                valid:
                  summary: Valid VAT ID with consultation number
                  value:
                    valid: true
                    vatId: IE6388047V
                    countryCode: IE
                    countryName: Ireland
                    companyName: GOOGLE IRELAND LIMITED
                    companyAddress: 3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4
                    companyRegistrationDate: null
                    companyForm: null
                    industryDescription: null
                    registryCode: null
                    registryCodeName: null
                    countryVat:
                      vatName: Value Added Tax
                      vatAbbr: VAT
                      currency: EUR
                      standardRate: 23
                      reducedRates: [13.5, 9]
                      superReducedRate: 4.8
                      parkingRate: 13.5
                      vatNumberFormat: IE + 7 digits + 1-2 letters
                      vatNumberPattern: '^IE[0-9]{7}[A-Z]{1,2}$'
                      countryVatUpdatedAt: '2026-04-23'
                    checkId: 018ef2a1-0000-7000-8000-000000000000
                    verifiedAt: '2026-04-23T10:15:30.123Z'
                    consultationNumber: WAPIAAAAW1234567
        '400':
          description: Invalid VAT ID format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: |
            The configured requester VAT (dashboard setting or query
            params) is invalid in VIES. Only returned when a requester is
            supplied. Fix or clear it to continue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'
        '429':
          description: Monthly quota exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'
        '502':
          description: VIES returned an unexpected protocol error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'
        '503':
          description: |
            VIES is unavailable. Without a requester, vatnode also
            exhausted national-registry fallbacks. With a requester,
            national fallback is intentionally skipped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'
        '504':
          description: VIES request timed out and no fallback was available.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'

  /v1/rates:
    get:
      tags: [VAT rates]
      summary: List VAT rates for all EU member states
      operationId: listRates
      security: []
      responses:
        '200':
          description: List of VAT rates for every EU-27 country.
          content:
            application/json:
              schema:
                type: object
                required: [rates, count]
                properties:
                  rates:
                    type: array
                    items:
                      $ref: '#/components/schemas/VatRate'
                  count:
                    type: integer
                    example: 27

  /v1/rates/{countryCode}:
    get:
      tags: [VAT rates]
      summary: Get VAT rates for a single country
      operationId: getRate
      security: []
      parameters:
        - name: countryCode
          in: path
          required: true
          description: Two-letter country code (uppercase). Greece is `EL`, not `GR`.
          schema:
            type: string
            minLength: 2
            maxLength: 2
            example: DE
      responses:
        '200':
          description: VAT rates for the country.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatRate'
        '404':
          description: Unknown country code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VatError'

components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: vn_live_* or vn_test_*
      description: |
        API key issued from the vatnode dashboard. Live keys hit VIES and
        consume monthly quota. Test keys accept only VAT IDs starting with
        `XX` and return deterministic fixtures.

  parameters:
    VatIdPath:
      name: vatId
      in: path
      required: true
      description: |
        VAT ID including the two-letter country prefix, e.g. `IE6388047V`.
        Whitespace and separators are normalised server-side.
      schema:
        type: string
        example: IE6388047V

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/VatError'

  schemas:
    VatValidationResult:
      type: object
      required:
        - valid
        - vatId
        - countryCode
        - countryName
        - companyName
        - companyAddress
        - companyRegistrationDate
        - companyForm
        - industryDescription
        - registryCode
        - registryCodeName
        - countryVat
        - checkId
        - verifiedAt
        - consultationNumber
      properties:
        valid:
          type: boolean
          description: Whether the VAT ID is valid per VIES or national fallback.
        vatId:
          type: string
          description: Normalised VAT ID (uppercase, no separators).
        countryCode:
          type: string
          description: Two-letter country code extracted from the VAT ID.
        countryName:
          type: string
        companyName:
          type: [string, 'null']
        companyAddress:
          type: [string, 'null']
        companyRegistrationDate:
          type: [string, 'null']
          format: date
        companyForm:
          type: [string, 'null']
        industryDescription:
          type: [string, 'null']
        registryCode:
          type: [string, 'null']
        registryCodeName:
          type: [string, 'null']
        countryVat:
          $ref: '#/components/schemas/CountryVatInfo'
        checkId:
          type: string
          format: uuid
          description: UUIDv7 identifier for this validation.
        verifiedAt:
          type: string
          format: date-time
        consultationNumber:
          type: [string, 'null']
          description: |
            VIES `requestIdentifier`, returned only when both
            `requesterCountryCode` and `requesterVatNumber` were supplied
            and VIES answered successfully via `checkVatApprox`. `null`
            when no requester was provided, when the national fallback
            was used, or when served from cache.

    CountryVatInfo:
      type: object
      required:
        - vatName
        - vatAbbr
        - currency
        - standardRate
        - reducedRates
        - superReducedRate
        - parkingRate
        - vatNumberFormat
        - vatNumberPattern
        - countryVatUpdatedAt
      properties:
        vatName:
          type: string
          example: Value Added Tax
        vatAbbr:
          type: string
          example: VAT
        currency:
          type: string
          example: EUR
        standardRate:
          type: number
          example: 23
        reducedRates:
          type: array
          items:
            type: number
          example: [13.5, 9]
        superReducedRate:
          type: [number, 'null']
          example: 4.8
        parkingRate:
          type: [number, 'null']
          example: 13.5
        vatNumberFormat:
          type: [string, 'null']
        vatNumberPattern:
          type: [string, 'null']
          description: Regular expression the VAT number should match.
        countryVatUpdatedAt:
          type: string
          description: Date the rate dataset was last updated (ISO 8601).

    VatRate:
      type: object
      required:
        - countryCode
        - countryName
        - vatName
        - vatAbbr
        - standardRate
        - reducedRates
        - superReducedRate
        - parkingRate
        - vatNumberFormat
        - vatNumberPattern
        - updatedAt
      properties:
        countryCode:
          type: string
          example: DE
        countryName:
          type: string
          example: Germany
        vatName:
          type: string
        vatAbbr:
          type: [string, 'null']
        standardRate:
          type: number
          example: 19
        reducedRates:
          type: array
          items:
            type: number
          example: [7]
        superReducedRate:
          type: [number, 'null']
        parkingRate:
          type: [number, 'null']
        vatNumberFormat:
          type: [string, 'null']
        vatNumberPattern:
          type: [string, 'null']
        updatedAt:
          type: string

    VatError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code:
              type: string
              enum:
                - INVALID_FORMAT
                - INVALID_REQUESTER
                - UNAUTHORIZED
                - RATE_LIMITED
                - VIES_UNAVAILABLE
                - VIES_ERROR
                - UPSTREAM_TIMEOUT
                - INTERNAL_ERROR
                - NOT_FOUND
              description: |
                Machine-readable error code.

                - `INVALID_FORMAT` — format invalid (local or VIES). HTTP 400.
                - `INVALID_REQUESTER` — requester VAT is invalid in VIES. Only raised when a requester is supplied. HTTP 422.
                - `UNAUTHORIZED` — missing or invalid API key. HTTP 401.
                - `RATE_LIMITED` — monthly quota exceeded. HTTP 429.
                - `VIES_UNAVAILABLE` — VIES/MS down; fallbacks also failed (no requester) or were skipped (requester set). HTTP 503.
                - `VIES_ERROR` — unexpected VIES protocol error. HTTP 502.
                - `UPSTREAM_TIMEOUT` — VIES timed out, no fallback available. HTTP 504.
                - `INTERNAL_ERROR` — unhandled server error. HTTP 500.
                - `NOT_FOUND` — unknown country code (rates only). HTTP 404.
            message:
              type: string
            requestId:
              type: string
              description: Echoes the `X-Request-Id` header for support correlation.
            viesCode:
              type: string
              description: Raw VIES error code when the underlying failure came from VIES.
            details:
              type: object
              additionalProperties: true
